Skip to content

Persistent and synced block damage #1261

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

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

Argmaster
Copy link
Collaborator

@Argmaster Argmaster commented Apr 1, 2025

This pull request is a proof of concept for persistent block damage synchronized between client and server.

I see at least few problems:

  • Current implementation works only for tools, completely ignores inflicting damage with hand or non-tool items. This can be easily fixed.
  • Current implementation syncs all block damage between server and all clients every tick (to implement decaying damage). Damage done by player mining is synced per block though. Possible fix: heal damaged blocks randomly and send only those healed this tick.
  • Every synchronization (so every game tick) causes all breaking animation faces to be removed and created again (again to guarantee decaying damage works).
  • Removing breaking animation faces has linear time complexity, combined with problems above this is highway to lag spikes, especially if we introduce explosives.
  • Current network stack may not be prepared for updates happening so often. Since every time you hit a block you have to send a request to server this would make mining with high latency exponentially more annoying.
  • Frequency of damaging blocks is determined purely on client side which means that modified client could break blocks nearly infinitely fast (although it is just one of many vulnerabilities in Cubyz)
  • There is no hard cap on how many blocks can be damaged at once. This again could be a problem after introducing explosions.

Related to: #1137

@Argmaster
Copy link
Collaborator Author

Cubyz.2025-04-01.01-14-31.mp4

@IntegratedQuantum
Copy link
Member

Current implementation syncs all block damage between server and all clients every tick (to implement decaying damage). Damage done by player mining is synced per block though. Possible fix: heal damaged blocks randomly and send only those healed this tick.

I think it's safe to assume that the timers are fairly synchronized, so we can just simulate the entire decay on the client side and only send updates when another player increases the breaking state.

Every synchronization (so every game tick) causes all breaking animation faces to be removed and created again (again to guarantee decaying damage works)

Ideally you'd only update those that actually changed their breaking animation frame.

@Argmaster Argmaster mentioned this pull request Apr 3, 2025
@Argmaster Argmaster mentioned this pull request Apr 16, 2025
16 tasks
@Argmaster Argmaster self-assigned this Apr 26, 2025
@Argmaster Argmaster added enhancement a new feature or improvement engine labels Apr 26, 2025
@Argmaster
Copy link
Collaborator Author

Alr, I guess I should open this PR for reviews, even though it will hurt, it is inevitable C:

@Argmaster Argmaster marked this pull request as ready for review April 27, 2025 17:10
src/blocks.zig Outdated
@@ -362,6 +365,10 @@ pub const Block = packed struct { // MARK: Block
return _touchFunction[self.typ];
}

pub inline fn healingRatio(_: Block) f32 {
return 0.05;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's a constant, then it should be a constant.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but it makes it necessary to use Block.healingRatio instead of a member function which is less syntacticaly pleasing (and that call which would be optimized away anyway) and makes it more complicated in case we would ever customize healing ration per-block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But having it as a member function implies that it already is customizable, which is misleading.

Also this is supposed to be an optimization and there should be no way to opt out of it.

src/renderer.zig Outdated
game.Player.selectionPosition1 = selectedPos;
main.network.Protocols.genericUpdate.sendWorldEditPos(main.game.world.?.conn, .selectedPos1, selectedPos);
return;
const selectedPos = selectedBlockPos orelse return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move all the cosmetic changes to another PR.


self.secondsSinceLastUpdate += deltaTime;
while(self.secondsSinceLastUpdate > updateIntervalSeconds) {
self._update(updateIntervalSeconds);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of updating everything in a big burst every second, maybe a random-tick technique could be used to distribute the load.

@@ -1019,11 +1171,29 @@ fn addBreakingAnimationFace(pos: Vec3i, quadIndex: main.models.QuadIndex, textur
});
}

fn removeBreakingAnimationFaces(pos: Vec3i) void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reorder things in a separate PR

fn removeBreakingAnimationFace(pos: Vec3i, quadIndex: main.models.QuadIndex, neighbor: ?chunk.Neighbor) void {
const worldPos = pos +% if(neighbor) |n| n.relPos() else Vec3i{0, 0, 0};
const relPos = worldPos & @as(Vec3i, @splat(main.chunk.chunkMask));
const mesh = getMeshAndIncreaseRefCount(.{.wx = worldPos[0], .wy = worldPos[1], .wz = worldPos[2], .voxelSize = 1}) orelse return;
defer mesh.decreaseRefCount();

mesh.mutex.lock();
defer mesh.mutex.unlock();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed now?

@@ -512,6 +511,8 @@ pub fn connectInternal(user: *User) void {
users.append(user);
userMutex.unlock();
user.conn.handShakeState.store(main.network.Protocols.handShake.stepComplete, .monotonic);

// main.network.Protocols.genericUpdate.sendDamageBlock(user.conn, .sync, null, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@@ -411,6 +411,120 @@ const WorldIO = struct { // MARK: WorldIO
}
};

pub const BlockDamage = struct { // MARK: BlockDamage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you merge the two versions in a generic way?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but why do you want to merge them? Literally everything else that has client and server side is coded separate if both sides are not identical, and here they are not. Where should this merged version be placed? How should it be merged?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to merge them, so I don't have to review how these two versions are different.
I just noticed they are using the same data structures, but if you say their implementation is sufficiently distinct (i.e. they are different beyond the fact that the client needs to add it to the mesh), then we can keep it like this.

@Argmaster Argmaster marked this pull request as draft April 29, 2025 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
engine enhancement a new feature or improvement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants