fix(sanctuary): resynchroniser les capes à la reconnexion (#13) #66
@@ -78,6 +78,14 @@ tasks.register("zombieNamesSmoke", JavaExec) {
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.client.SanctuaryZombieNamesSmoke"
|
||||
}
|
||||
|
||||
tasks.register("capeJoinSyncSmoke", JavaExec) {
|
||||
group = "verification"
|
||||
description = "Checks delayed, bounded cape snapshot retries after player reconnects."
|
||||
dependsOn tasks.named("testClasses")
|
||||
classpath = sourceSets.test.runtimeClasspath
|
||||
mainClass = "fr.koka99cab.sanctuary26.sanctuary.gameplay.CapeJoinSyncQueueSmoke"
|
||||
}
|
||||
|
||||
tasks.register("verifyBlackMarket") {
|
||||
group = "verification"
|
||||
description = "Checks the physical sign market, flash Shop, escrow ledger and mailbox delivery."
|
||||
@@ -229,6 +237,7 @@ tasks.register("verifySanctuary") {
|
||||
dependsOn tasks.named("progressionModelSmoke"), tasks.named("serverIdentityModelSmoke"),
|
||||
tasks.named("shopDeliveryTimingSmoke"), tasks.named("blackMarketModelSmoke"),
|
||||
tasks.named("questBoardModelSmoke"), tasks.named("shopHeaderLayoutSmoke"), tasks.named("zombieNamesSmoke"),
|
||||
tasks.named("capeJoinSyncSmoke"),
|
||||
tasks.named("verifyBlackMarket")
|
||||
|
||||
doLast {
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Bounded reconnect retry state for a player's complete cape snapshot. */
|
||||
final class CapeJoinSyncQueue {
|
||||
enum Decision { WAIT, SYNC, DROP }
|
||||
|
||||
private final int retryTicks;
|
||||
private final Map<UUID, Integer> remainingTicks = new LinkedHashMap<>();
|
||||
|
||||
CapeJoinSyncQueue(int retryTicks) {
|
||||
if (retryTicks < 1) throw new IllegalArgumentException("Cape retry window must be positive");
|
||||
this.retryTicks = retryTicks;
|
||||
}
|
||||
|
||||
void schedule(UUID playerId) {
|
||||
remainingTicks.put(playerId, retryTicks);
|
||||
}
|
||||
|
||||
List<UUID> pendingPlayerIds() {
|
||||
return List.copyOf(remainingTicks.keySet());
|
||||
}
|
||||
|
||||
Decision poll(UUID playerId, boolean connected, boolean channelReady) {
|
||||
Integer remaining = remainingTicks.get(playerId);
|
||||
if (remaining == null) return Decision.DROP;
|
||||
if (!connected) {
|
||||
remainingTicks.remove(playerId);
|
||||
return Decision.DROP;
|
||||
}
|
||||
if (channelReady) {
|
||||
remainingTicks.remove(playerId);
|
||||
return Decision.SYNC;
|
||||
}
|
||||
if (remaining <= 1) {
|
||||
remainingTicks.remove(playerId);
|
||||
return Decision.DROP;
|
||||
}
|
||||
remainingTicks.put(playerId, remaining - 1);
|
||||
return Decision.WAIT;
|
||||
}
|
||||
}
|
||||
+28
@@ -1,13 +1,18 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import fr.koka99cab.sanctuary26.sanctuary.network.CapeSyncPayload;
|
||||
import java.util.UUID;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
|
||||
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
|
||||
/** Server authority and multiplayer distribution for equipped cape visuals. */
|
||||
public final class SanctuaryCapeService {
|
||||
private static final int JOIN_SYNC_RETRY_TICKS = 100;
|
||||
private static final CapeJoinSyncQueue JOIN_SYNCS = new CapeJoinSyncQueue(JOIN_SYNC_RETRY_TICKS);
|
||||
private static boolean initialized;
|
||||
|
||||
private SanctuaryCapeService() {
|
||||
@@ -17,6 +22,7 @@ public final class SanctuaryCapeService {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> sync(newPlayer));
|
||||
ServerTickEvents.END_SERVER_TICK.register(SanctuaryCapeService::tickJoinSyncs);
|
||||
}
|
||||
|
||||
public static void join(ServerPlayer joined) {
|
||||
@@ -24,6 +30,10 @@ public final class SanctuaryCapeService {
|
||||
send(joined, online);
|
||||
}
|
||||
sync(joined);
|
||||
// JOIN can run while the reconnecting client's play channel or player list is
|
||||
// still settling. Replay the complete snapshot on a later server tick so a
|
||||
// missed initial packet never requires removing and re-equipping the cape.
|
||||
JOIN_SYNCS.schedule(joined.getUUID());
|
||||
}
|
||||
|
||||
public static void sync(ServerPlayer owner) {
|
||||
@@ -32,6 +42,24 @@ public final class SanctuaryCapeService {
|
||||
}
|
||||
}
|
||||
|
||||
private static void tickJoinSyncs(MinecraftServer server) {
|
||||
for (UUID playerId : JOIN_SYNCS.pendingPlayerIds()) {
|
||||
ServerPlayer receiver = server.getPlayerList().getPlayer(playerId);
|
||||
CapeJoinSyncQueue.Decision decision = JOIN_SYNCS.poll(playerId, receiver != null,
|
||||
receiver != null && ServerPlayNetworking.canSend(receiver, CapeSyncPayload.ID));
|
||||
if (decision == CapeJoinSyncQueue.Decision.SYNC) sendSnapshot(receiver);
|
||||
}
|
||||
}
|
||||
|
||||
private static void sendSnapshot(ServerPlayer receiver) {
|
||||
boolean includedReceiver = false;
|
||||
for (ServerPlayer owner : PlayerLookup.all(receiver.level().getServer())) {
|
||||
send(receiver, owner);
|
||||
includedReceiver |= owner.getUUID().equals(receiver.getUUID());
|
||||
}
|
||||
if (!includedReceiver) send(receiver, receiver);
|
||||
}
|
||||
|
||||
private static void send(ServerPlayer receiver, ServerPlayer owner) {
|
||||
if (!ServerPlayNetworking.canSend(receiver, CapeSyncPayload.ID)) return;
|
||||
String capeId = SanctuaryCapes.itemId(SanctuaryInventoryCapacity.equippedCape(owner));
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package fr.koka99cab.sanctuary26.sanctuary.gameplay;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Executable coverage for delayed and bounded cape synchronization after reconnect. */
|
||||
public final class CapeJoinSyncQueueSmoke {
|
||||
private CapeJoinSyncQueueSmoke() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
CapeJoinSyncQueue queue = new CapeJoinSyncQueue(3);
|
||||
UUID playerId = UUID.fromString("12345678-1234-5678-9abc-def012345678");
|
||||
|
||||
queue.schedule(playerId);
|
||||
require(queue.pendingPlayerIds().size() == 1, "Reconnect did not schedule one cape snapshot");
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.WAIT,
|
||||
"Unavailable play channel was not retried");
|
||||
queue.schedule(playerId);
|
||||
require(queue.pendingPlayerIds().size() == 1, "Repeated join duplicated the cape retry");
|
||||
require(queue.poll(playerId, true, true) == CapeJoinSyncQueue.Decision.SYNC,
|
||||
"Ready play channel did not release the cape snapshot");
|
||||
require(queue.pendingPlayerIds().isEmpty(), "Successful cape sync stayed pending");
|
||||
|
||||
queue.schedule(playerId);
|
||||
require(queue.poll(playerId, false, false) == CapeJoinSyncQueue.Decision.DROP,
|
||||
"Disconnected player retained a cape retry");
|
||||
|
||||
queue.schedule(playerId);
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.WAIT,
|
||||
"Cape retry window ended one tick too early");
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.WAIT,
|
||||
"Cape retry window ended two ticks too early");
|
||||
require(queue.poll(playerId, true, false) == CapeJoinSyncQueue.Decision.DROP,
|
||||
"Cape retry escaped its bounded window");
|
||||
require(queue.pendingPlayerIds().isEmpty(), "Expired cape retry stayed pending");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user