Skip to content

Commit f06e860

Browse files
committed
MP fence clear and sychronisation fixed
1 parent 0612745 commit f06e860

8 files changed

Lines changed: 407 additions & 14 deletions

File tree

docs/test-fence-sync.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
player1# Fence Synchronization Test
2+
3+
## Issues Fixed
4+
5+
### Issue 1: Fence Creation Not Synchronized
6+
Players in multiplayer mode could not see each other's fences.
7+
8+
**Root Cause**: The local Player object was not getting its player ID set when connecting to multiplayer, causing all fence placement messages to use "local_player" as the owner ID instead of the actual client ID.
9+
10+
**Fix Applied**: Modified `GameMessageHandler.java` to set the player ID on the local Player object when receiving the connection success message.
11+
12+
### Issue 2: Fence Deletion Not Synchronized
13+
Players could see each other create fences, but fence deletions were not synchronized between players.
14+
15+
**Root Cause**: When removing fences, the client was generating a new random UUID for the fence ID instead of using the actual fence ID of the piece being removed. This caused the server to reject the removal request because it couldn't find a fence with that random ID.
16+
17+
**Fix Applied**:
18+
1. Added fence ID support to `FencePiece` class
19+
2. Modified `FencePieceFactory` to accept fence IDs
20+
3. Updated `FenceStructureManager` to store and use fence IDs
21+
4. Fixed `FenceBuildingManager` to use the correct fence ID when removing pieces
22+
5. Updated client-side fence processing to handle fence IDs properly
23+
24+
### Issue 3: Clear All Fences (C Key) Not Synchronized
25+
Players could see individual fence deletions, but when a player pressed "C" to clear an entire enclosure, other players could not see the mass deletion.
26+
27+
**Root Cause**: The `clearNearestEnclosure()` method was only removing fences locally without sending network messages to other players. It bypassed the normal removal flow that sends `FenceRemoveMessage` to the server.
28+
29+
**Fix Applied**:
30+
1. Modified `clearNearestEnclosure()` method in `FenceBuildingManager` to send individual `FenceRemoveMessage` for each fence piece being removed
31+
2. Fixed `replacePieceType()` method in `FenceStructureManager` to preserve fence IDs when updating adjacent pieces
32+
3. Enhanced server validation in `ClientConnection` to allow fence removal with fallback IDs when the player owns the fence, preventing rejection of legitimate removal requests during enclosure clearing
33+
34+
### Issue 4: Clear All Fences (C Key) Not Respecting Ownership
35+
Players could clear fence enclosures created by other players using the "C" key, which is a security issue.
36+
37+
**Root Cause**: The `clearNearestEnclosure()` method was removing ALL connected fence pieces without checking ownership, allowing players to delete fences they didn't create.
38+
39+
**Fix Applied**: Added ownership validation to `clearNearestEnclosure()` method:
40+
1. Check if the closest fence piece is owned by the current player before proceeding
41+
2. Filter connected pieces to only include those owned by the current player
42+
3. Only remove fence pieces that belong to the requesting player
43+
4. Provide clear feedback when no owned pieces are found in the enclosure
44+
45+
## Changes Made
46+
47+
### Core Classes Modified:
48+
- `FencePiece.java` - Added fenceId field and methods
49+
- `FencePieceFactory.java` - Added createPiece method with fence ID parameter
50+
- `FenceStructureManager.java` - Added fence ID support to addFencePiece method
51+
- `FenceBuildingManager.java` - Fixed fence ID handling for both creation and removal
52+
- `MyGdxGame.java` - Updated fence processing methods to use fence IDs
53+
- `GameMessageHandler.java` - Added player ID assignment on connection
54+
55+
### Key Changes:
56+
1. **Fence Creation**: Generate fence ID once and use it for both local fence piece and network message
57+
2. **Fence Removal**: Use the actual fence ID from the removed piece instead of generating a new one
58+
3. **Network Sync**: Pass fence IDs through all fence synchronization messages
59+
4. **Player ID**: Set correct player ID on connection to avoid "local_player" fallback
60+
5. **Fence ID Preservation**: Maintain fence IDs when updating adjacent piece types
61+
6. **Fallback ID Handling**: Allow server to accept fence removal with fallback IDs for legitimate owners
62+
63+
## Test Steps
64+
1. Start Player1 as host server
65+
2. Connect Player2 to localhost:25565
66+
3. Both players activate Free World mode
67+
4. **Fence Creation Test**:
68+
- Player1 builds a fence → Player2 should see it immediately
69+
- Player2 builds a fence → Player1 should see it immediately
70+
5. **Individual Fence Deletion Test**:
71+
- Player1 removes a fence (right-click) → Player2 should see it disappear immediately
72+
- Player2 removes a fence (right-click) → Player1 should see it disappear immediately
73+
6. **Enclosure Clear Test**:
74+
- Player1 builds a complete fence enclosure
75+
- Player2 should see the entire enclosure
76+
- Player1 presses "C" to clear the enclosure → Player2 should see all fences disappear immediately
77+
- Player2 builds an enclosure and presses "C" → Player1 should see it clear immediately
78+
7. **Ownership Validation Test**:
79+
- Player1 builds a fence enclosure
80+
- Player2 tries to press "C" near Player1's enclosure → Should NOT clear Player1's fences
81+
- Player2 builds their own enclosure next to Player1's
82+
- Player2 presses "C" → Should only clear Player2's fences, leaving Player1's intact
83+
- Player1 presses "C" → Should only clear Player1's remaining fences
84+
85+
## Expected Behavior
86+
- ✅ Each player sees other players' fence creations in real-time
87+
- ✅ Each player sees other players' individual fence deletions in real-time
88+
- ✅ Each player sees other players' enclosure clearing (C key) in real-time
89+
- ✅ Fence placement messages use correct client IDs
90+
- ✅ Fence removal messages use correct fence IDs
91+
- ✅ No more "local_player" fallback in multiplayer mode
92+
- ✅ Server properly validates fence ownership before allowing removal
93+
- ✅ Mass fence clearing sends individual removal messages for each piece
94+
- ✅ Players can only clear their own fence enclosures (ownership validation)
95+
- ✅ Mixed-ownership enclosures only clear pieces owned by the requesting player
96+
- ✅ Clear feedback when attempting to clear unowned fences

src/main/java/wagemaker/uk/fence/FenceBuildingManager.java

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.badlogic.gdx.math.Vector2;
1010
import com.badlogic.gdx.math.Vector3;
1111
import java.awt.Point;
12+
import java.util.HashSet;
1213
import java.util.List;
1314
import java.util.Map;
1415
import java.util.Set;
@@ -414,11 +415,30 @@ public boolean placeFenceSegment(int gridX, int gridY) {
414415

415416
try {
416417
System.out.println("[FenceBuildingManager] Attempting to place fence at grid (" + gridPos.x + ", " + gridPos.y + ") with material " + selectedMaterialType + " for owner " + playerId);
418+
419+
// Generate fence ID for both local and network use
420+
String fenceId = java.util.UUID.randomUUID().toString();
421+
417422
// Attempt to place the fence piece with automatic piece type selection
418-
FencePiece placedPiece = structureManager.addFencePiece(gridPos, selectedMaterialType, playerId);
423+
FencePiece placedPiece = structureManager.addFencePiece(gridPos, selectedMaterialType, playerId, fenceId);
419424

420425
if (placedPiece != null) {
421-
System.out.println("[FenceBuildingManager] Fence piece created: " + placedPiece.getType() + " at world (" + placedPiece.getX() + ", " + placedPiece.getY() + ")");
426+
System.out.println("[FenceBuildingManager] Fence piece created: " + placedPiece.getType() + " at world (" + placedPiece.getX() + ", " + placedPiece.getY() + ") with ID " + fenceId);
427+
428+
// Send fence place message to server in multiplayer mode
429+
wagemaker.uk.network.GameClient gameClient = player.getGameClient();
430+
System.out.println("DEBUG: Sending fence place message - gameClient=" + (gameClient != null ? "exists" : "null") +
431+
", connected=" + (gameClient != null ? gameClient.isConnected() : "N/A"));
432+
if (gameClient != null && gameClient.isConnected()) {
433+
wagemaker.uk.network.FencePlaceMessage fenceMsg = new wagemaker.uk.network.FencePlaceMessage(
434+
playerId, fenceId, gridX, gridY, placedPiece.getType(), selectedMaterialType, playerId
435+
);
436+
gameClient.sendMessage(fenceMsg);
437+
System.out.println("DEBUG: Sent FencePlaceMessage to server: " + fenceMsg);
438+
} else {
439+
System.out.println("DEBUG: NOT sending fence message - gameClient null or not connected");
440+
}
441+
422442
// Consume materials from inventory
423443
FenceMaterialProvider materialProvider = validator.getMaterialProvider();
424444
if (materialProvider != null) {
@@ -555,6 +575,28 @@ public boolean removeFenceSegment(int gridX, int gridY) {
555575
FencePiece removedPiece = structureManager.removeFencePiece(gridPos);
556576

557577
if (removedPiece != null) {
578+
// Get the fence ID from the removed piece
579+
String fenceId = removedPiece.getFenceId();
580+
if (fenceId == null) {
581+
// Fallback for pieces without IDs (shouldn't happen in multiplayer)
582+
fenceId = "unknown-" + System.currentTimeMillis();
583+
System.out.println("WARNING: Removed fence piece had no ID, using fallback: " + fenceId);
584+
}
585+
586+
// Send fence remove message to server in multiplayer mode
587+
wagemaker.uk.network.GameClient gameClient = player.getGameClient();
588+
System.out.println("DEBUG: Sending fence remove message - gameClient=" + (gameClient != null ? "exists" : "null") +
589+
", connected=" + (gameClient != null ? gameClient.isConnected() : "N/A"));
590+
if (gameClient != null && gameClient.isConnected()) {
591+
wagemaker.uk.network.FenceRemoveMessage fenceMsg = new wagemaker.uk.network.FenceRemoveMessage(
592+
playerId, fenceId, gridX, gridY, materialToReturn, playerId
593+
);
594+
gameClient.sendMessage(fenceMsg);
595+
System.out.println("DEBUG: Sent FenceRemoveMessage to server with fence ID: " + fenceId);
596+
} else {
597+
System.out.println("DEBUG: NOT sending fence remove message - gameClient null or not connected");
598+
}
599+
558600
// Return materials to inventory
559601
FenceMaterialProvider materialProvider = validator.getMaterialProvider();
560602
if (materialProvider != null) {
@@ -922,20 +964,65 @@ public void clearNearestEnclosure() {
922964
}
923965

924966
if (closestPos != null) {
967+
// Get the current player's ID for ownership validation
968+
String playerId = player.getPlayerId();
969+
if (playerId == null) {
970+
playerId = "local_player";
971+
}
972+
973+
// Check if the closest piece is owned by the current player
974+
FencePiece closestPiece = structureManager.getFencePiece(closestPos);
975+
if (closestPiece == null || !playerId.equals(closestPiece.getOwnerId())) {
976+
System.out.println("[FenceBuildingManager] Cannot clear enclosure - closest fence piece is not owned by current player");
977+
return;
978+
}
979+
925980
// Get connected pieces (the enclosure)
926981
Set<Point> connectedPoints = structureManager.findConnectedPieces(closestPos);
927982

928983
if (!connectedPoints.isEmpty()) {
929-
System.out.println("[FenceBuildingManager] Clearing enclosure of " + connectedPoints.size() + " pieces nearest to " + closestPos);
984+
// Filter to only include pieces owned by the current player
985+
Set<Point> ownedPieces = new HashSet<>();
986+
for (Point pos : connectedPoints) {
987+
FencePiece piece = structureManager.getFencePiece(pos);
988+
if (piece != null && playerId.equals(piece.getOwnerId())) {
989+
ownedPieces.add(pos);
990+
}
991+
}
992+
993+
if (ownedPieces.isEmpty()) {
994+
System.out.println("[FenceBuildingManager] Cannot clear enclosure - no pieces owned by current player");
995+
return;
996+
}
997+
998+
System.out.println("[FenceBuildingManager] Clearing " + ownedPieces.size() + " owned pieces out of " + connectedPoints.size() + " total pieces nearest to " + closestPos);
930999

9311000
int removedCount = 0;
9321001

933-
// Remove each piece
934-
for (Point pos : connectedPoints) {
1002+
// Remove each owned piece and send network messages
1003+
for (Point pos : ownedPieces) {
9351004
FencePiece removedPiece = structureManager.removeFencePiece(pos);
9361005
if (removedPiece != null) {
9371006
updateCollisionBoundaries(pos, false);
9381007
removedCount++;
1008+
1009+
// Send fence remove message to server in multiplayer mode
1010+
1011+
wagemaker.uk.network.GameClient gameClient = player.getGameClient();
1012+
if (gameClient != null && gameClient.isConnected()) {
1013+
String fenceId = removedPiece.getFenceId();
1014+
if (fenceId == null) {
1015+
fenceId = "unknown-" + System.currentTimeMillis() + "-" + pos.x + "-" + pos.y;
1016+
System.out.println("WARNING: Removed fence piece had no ID, using fallback: " + fenceId);
1017+
}
1018+
1019+
wagemaker.uk.network.FenceRemoveMessage fenceMsg = new wagemaker.uk.network.FenceRemoveMessage(
1020+
playerId, fenceId, pos.x, pos.y, selectedMaterialType, playerId
1021+
);
1022+
gameClient.sendMessage(fenceMsg);
1023+
System.out.println("DEBUG: Sent FenceRemoveMessage for enclosure clear - fence ID: " + fenceId + " at (" + pos.x + ", " + pos.y + ")");
1024+
}
1025+
9391026
// Trigger visual effect for each piece
9401027
if (visualEffectsManager != null) {
9411028
visualEffectsManager.triggerRemovalAnimation(pos, selectedMaterialType);

src/main/java/wagemaker/uk/fence/FencePiece.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public abstract class FencePiece {
1919
protected static final String TEXTURE_PATH = "textures/fense.png";
2020

2121
protected String ownerId;
22+
protected String fenceId;
2223

2324
/**
2425
* Creates a new fence piece at the specified position.
@@ -27,7 +28,7 @@ public abstract class FencePiece {
2728
* @param type The type of fence piece
2829
*/
2930
public FencePiece(float x, float y, FencePieceType type) {
30-
this(x, y, type, null);
31+
this(x, y, type, null, null);
3132
}
3233

3334
/**
@@ -38,10 +39,23 @@ public FencePiece(float x, float y, FencePieceType type) {
3839
* @param ownerId The ID of the owner of this fence piece
3940
*/
4041
public FencePiece(float x, float y, FencePieceType type, String ownerId) {
42+
this(x, y, type, ownerId, null);
43+
}
44+
45+
/**
46+
* Creates a new fence piece at the specified position with ownership and fence ID.
47+
* @param x World X coordinate
48+
* @param y World Y coordinate
49+
* @param type The type of fence piece
50+
* @param ownerId The ID of the owner of this fence piece
51+
* @param fenceId The unique ID of this fence piece
52+
*/
53+
public FencePiece(float x, float y, FencePieceType type, String ownerId, String fenceId) {
4154
this.x = x;
4255
this.y = y;
4356
this.type = type;
4457
this.ownerId = ownerId;
58+
this.fenceId = fenceId;
4559
loadTexture();
4660
}
4761

@@ -172,6 +186,22 @@ public void setOwnerId(String ownerId) {
172186
this.ownerId = ownerId;
173187
}
174188

189+
/**
190+
* Gets the fence ID of this fence piece.
191+
* @return Fence ID, or null if not set
192+
*/
193+
public String getFenceId() {
194+
return fenceId;
195+
}
196+
197+
/**
198+
* Sets the fence ID of this fence piece.
199+
* @param fenceId New fence ID
200+
*/
201+
public void setFenceId(String fenceId) {
202+
this.fenceId = fenceId;
203+
}
204+
175205
/**
176206
* Disposes of the texture resources used by this fence piece.
177207
* Should be called when the fence piece is no longer needed.

src/main/java/wagemaker/uk/fence/FencePieceFactory.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,22 @@ public static FencePiece createPiece(FencePieceType type, float x, float y, Stri
5656
piece.setOwnerId(ownerId);
5757
return piece;
5858
}
59+
60+
/**
61+
* Creates a fence piece of the specified type at the given position with ownership and fence ID.
62+
* @param type The type of fence piece to create
63+
* @param x World X coordinate
64+
* @param y World Y coordinate
65+
* @param ownerId The ID of the owner
66+
* @param fenceId The unique ID of the fence piece
67+
* @return A new FencePiece instance of the specified type
68+
*/
69+
public static FencePiece createPiece(FencePieceType type, float x, float y, String ownerId, String fenceId) {
70+
FencePiece piece = createPiece(type, x, y);
71+
piece.setOwnerId(ownerId);
72+
piece.setFenceId(fenceId);
73+
return piece;
74+
}
5975

6076
/**
6177
* Gets the sequence of fence piece types needed for a rectangular enclosure.

src/main/java/wagemaker/uk/fence/FenceStructureManager.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ public FencePiece addFencePiece(Point gridPos, FenceMaterialType materialType) {
9090
* @throws IllegalArgumentException if the position is already occupied
9191
*/
9292
public FencePiece addFencePiece(Point gridPos, FenceMaterialType materialType, String ownerId) {
93+
return addFencePiece(gridPos, materialType, ownerId, null);
94+
}
95+
96+
/**
97+
* Adds a fence piece at the specified grid position with ownership and fence ID.
98+
* Automatically determines the correct piece type based on adjacent pieces.
99+
* Updates connections with adjacent pieces.
100+
*
101+
* @param gridPos Grid position where to place the fence piece
102+
* @param materialType Type of material to use for the fence piece
103+
* @param ownerId ID of the player who owns this fence piece
104+
* @param fenceId Unique ID of the fence piece
105+
* @return The created FencePiece, or null if placement failed
106+
* @throws IllegalArgumentException if the position is already occupied
107+
*/
108+
public FencePiece addFencePiece(Point gridPos, FenceMaterialType materialType, String ownerId, String fenceId) {
93109
if (placedFences.containsKey(gridPos)) {
94110
throw new IllegalArgumentException("Position " + gridPos + " is already occupied");
95111
}
@@ -106,7 +122,7 @@ public FencePiece addFencePiece(Point gridPos, FenceMaterialType materialType, S
106122
com.badlogic.gdx.math.Vector2 worldPos = grid.gridToWorld(gridPos);
107123

108124
// Create the fence piece
109-
FencePiece piece = FencePieceFactory.createPiece(pieceType, worldPos.x, worldPos.y, ownerId);
125+
FencePiece piece = FencePieceFactory.createPiece(pieceType, worldPos.x, worldPos.y, ownerId, fenceId);
110126

111127
// Add to our data structures
112128
placedFences.put(new Point(gridPos.x, gridPos.y), piece);
@@ -316,9 +332,9 @@ private void replacePieceType(Point gridPos, FencePieceType newType) {
316332
return; // Nothing to replace
317333
}
318334

319-
// Create new piece with the same position and owner
335+
// Create new piece with the same position, owner, and fence ID
320336
com.badlogic.gdx.math.Vector2 worldPos = grid.gridToWorld(gridPos);
321-
FencePiece newPiece = FencePieceFactory.createPiece(newType, worldPos.x, worldPos.y, oldPiece.getOwnerId());
337+
FencePiece newPiece = FencePieceFactory.createPiece(newType, worldPos.x, worldPos.y, oldPiece.getOwnerId(), oldPiece.getFenceId());
322338

323339
// Replace in the map
324340
placedFences.put(gridPos, newPiece);

0 commit comments

Comments
 (0)