···3131local lastRaycastFailure: string? = nil
3232local lastSelectedChunkKey: string? = nil
3333local lastSelectedBlockKey: string? = nil
3434+local duplicateResyncCooldown: {[string]: number} = {}
3435local BREAK_ROLLBACK_TIMEOUT = 0.6
3536local pendingBreaks = {}
3637local clearSelection
···426427 return
427428 end
428429429429- -- if the client already thinks this block is the same id, skip sending
430430+ -- allow sending even if the client thinks the id matches; server truth wins
430431 if chunk then
431432 local existing = chunk:GetBlockAt(x, y, z)
432433 local existingId = existing and existing.id
433434 if existingId and tostring(existingId) == tostring(blockId) then
434435 debugPlacementLog(
435435- "[PLACE][CLIENT][SKIP]",
436436- "duplicate id",
436436+ "[PLACE][CLIENT][DUPLICATE]",
437437+ "still sending",
437438 "chunk",
438439 cx,
439440 cy,
···447448 "blockId",
448449 blockId
449450 )
450450- return
451451+ local ck = makeChunkKey(cx, cy, cz)
452452+ local last = duplicateResyncCooldown[ck]
453453+ if not last or (tick() - last) > 0.5 then
454454+ duplicateResyncCooldown[ck] = tick()
455455+ task.defer(function()
456456+ ChunkManager:RefreshChunk(cx, cy, cz)
457457+ end)
458458+ end
451459 else
452460 debugPlacementLog(
453461 "[PLACE][CLIENT][EXISTING]",