2025-01-29 18:25:34 -05:00
|
|
|
package kamkeel.npcs.controllers;
|
|
|
|
|
|
|
|
|
|
import cpw.mods.fml.relauncher.Side;
|
|
|
|
|
import cpw.mods.fml.relauncher.SideOnly;
|
2025-10-19 22:06:11 -04:00
|
|
|
import io.netty.buffer.ByteBuf;
|
|
|
|
|
import io.netty.buffer.Unpooled;
|
2026-01-22 23:07:38 -05:00
|
|
|
import kamkeel.npcs.addon.DBCAddon;
|
2026-03-31 21:44:23 +02:00
|
|
|
import kamkeel.npcs.controllers.sync.SyncHandler;
|
|
|
|
|
import kamkeel.npcs.controllers.sync.SyncRegistry;
|
|
|
|
|
import kamkeel.npcs.controllers.sync.handlers.*;
|
2025-10-19 22:06:11 -04:00
|
|
|
import kamkeel.npcs.network.LargeAbstractPacket;
|
2025-01-29 18:25:34 -05:00
|
|
|
import kamkeel.npcs.network.PacketHandler;
|
2025-02-20 21:40:39 -05:00
|
|
|
import kamkeel.npcs.network.enums.EnumSyncAction;
|
2026-03-31 19:59:53 +02:00
|
|
|
import kamkeel.npcs.network.enums.SyncType;
|
2025-02-18 22:22:40 -05:00
|
|
|
import kamkeel.npcs.network.packets.data.LoginPacket;
|
2026-03-11 21:42:27 -04:00
|
|
|
import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket;
|
2025-01-30 13:54:49 -05:00
|
|
|
import kamkeel.npcs.network.packets.data.large.SyncPacket;
|
2025-06-24 05:41:34 -04:00
|
|
|
import kamkeel.npcs.network.packets.request.party.PartyInfoPacket;
|
2025-10-19 22:06:11 -04:00
|
|
|
import kamkeel.npcs.util.ByteBufUtils;
|
2025-01-29 18:25:34 -05:00
|
|
|
import net.minecraft.entity.player.EntityPlayerMP;
|
|
|
|
|
import net.minecraft.nbt.NBTTagCompound;
|
2025-10-19 22:06:11 -04:00
|
|
|
import net.minecraft.server.MinecraftServer;
|
2026-03-31 22:55:42 +02:00
|
|
|
import noppes.npcs.LogWriter;
|
2025-01-29 18:25:34 -05:00
|
|
|
import noppes.npcs.client.ClientCacheHandler;
|
2025-07-23 14:22:52 -04:00
|
|
|
import noppes.npcs.controllers.data.PlayerData;
|
2025-01-29 18:25:34 -05:00
|
|
|
|
|
|
|
|
import java.util.HashMap;
|
2026-01-30 05:01:54 -05:00
|
|
|
import java.util.LinkedHashMap;
|
2025-10-19 22:06:11 -04:00
|
|
|
import java.util.Map;
|
2026-03-31 19:59:53 +02:00
|
|
|
import java.util.Set;
|
2025-10-19 22:06:11 -04:00
|
|
|
import java.util.UUID;
|
2025-02-10 21:41:54 -05:00
|
|
|
import java.util.concurrent.ConcurrentHashMap;
|
2025-10-19 22:06:11 -04:00
|
|
|
import java.util.function.Supplier;
|
2025-01-29 18:25:34 -05:00
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
/**
|
|
|
|
|
* Central sync backend for CNPC+.
|
|
|
|
|
*
|
|
|
|
|
* <p>This controller manages two distinct sync lanes:</p>
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li><b>Global cached registry families</b> — registered via {@link SyncRegistry}
|
|
|
|
|
* with concrete {@link SyncHandler} implementations. These families use
|
|
|
|
|
* cached serialized payloads, revision-based delta sync on reconnect,
|
|
|
|
|
* and chunked delivery. All family-specific RELOAD/UPDATE/REMOVE logic
|
|
|
|
|
* lives in the handler implementations, not here.</li>
|
|
|
|
|
* <li><b>Direct per-player sync helpers</b> — for transient per-player state
|
|
|
|
|
* like effects, ability hotbars, and cooldowns. These bypass the cached
|
|
|
|
|
* registry system and are sent as direct packets. Owned by
|
|
|
|
|
* {@link PlayerEffectSyncHelper} and {@link PlayerAbilitySyncHelper}.</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
*
|
|
|
|
|
* <p>This class retains ownership of:</p>
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>Cache entry storage and lazy rebuild ({@link SyncCacheEntry})</li>
|
|
|
|
|
* <li>Cached payload byte[] + precomputed chunks ({@link CachedSyncPayload})</li>
|
|
|
|
|
* <li>Per-player revision tracking ({@link PlayerSyncState})</li>
|
|
|
|
|
* <li>Login handshake and post-login packet sequencing</li>
|
|
|
|
|
* <li>Generic sync dispatch helpers (syncAll, syncUpdate, syncRemove)</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
*/
|
2025-01-30 09:26:04 -05:00
|
|
|
public class SyncController {
|
2025-01-29 18:25:34 -05:00
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
/** Cached serialized reload payloads and revision state for each global sync family. */
|
2026-03-31 19:59:53 +02:00
|
|
|
private static final Map<SyncType, SyncCacheEntry> cacheEntries = new LinkedHashMap<>();
|
2025-11-05 17:23:21 -05:00
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
/** Last known synced revision map for each connected player. */
|
|
|
|
|
private static final ConcurrentHashMap<UUID, PlayerSyncState> playerSyncState = new ConcurrentHashMap<>();
|
2025-11-05 17:23:21 -05:00
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
/** Stable identity token for the current dedicated-server process, used by the revision handshake. */
|
|
|
|
|
private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString();
|
2025-10-19 22:06:11 -04:00
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Lifecycle — handler registration and cache initialization
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2025-10-19 22:06:11 -04:00
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
/**
|
|
|
|
|
* Registers all core CNPC+ sync handlers in deterministic order.
|
|
|
|
|
* Called on server start. The registration order determines login
|
|
|
|
|
* sync ordering via {@link SyncRegistry#getLoginTypes()}.
|
|
|
|
|
*
|
|
|
|
|
* <p>Handler registration order matches the original hard-coded
|
|
|
|
|
* LOGIN_SYNC_TYPES array for behavioral parity.</p>
|
|
|
|
|
*/
|
2025-11-05 17:23:21 -05:00
|
|
|
public static void load() {
|
2025-11-05 17:33:24 -05:00
|
|
|
cacheEntries.clear();
|
2025-11-05 17:23:21 -05:00
|
|
|
playerSyncState.clear();
|
2026-03-31 21:44:23 +02:00
|
|
|
SyncRegistry.clear();
|
|
|
|
|
|
|
|
|
|
// Register all core handlers in deterministic order
|
|
|
|
|
// Order matches original LOGIN_SYNC_TYPES for login sync ordering
|
|
|
|
|
SyncRegistry.register(SyncType.FACTION, new FactionSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.DIALOG, new DialogSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.DIALOG_CATEGORY, new DialogCategorySyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.QUEST, new QuestSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.QUEST_CATEGORY, new QuestCategorySyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.PLAYERDATA, new PlayerDataSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.WORKBENCH_RECIPES, new WorkbenchRecipeSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.CARPENTRY_RECIPES, new CarpentryRecipeSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.ANVIL_RECIPES, new AnvilRecipeSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.CUSTOM_EFFECTS, new CustomEffectSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.MAGIC, new MagicSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.MAGIC_CYCLE, new MagicCycleSyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.CUSTOM_ABILITY, new CustomAbilitySyncHandler());
|
|
|
|
|
SyncRegistry.register(SyncType.CHAINED_ABILITY, new ChainedAbilitySyncHandler());
|
|
|
|
|
|
|
|
|
|
// Register caches for all cached (login-sync) types
|
|
|
|
|
// The cache supplier delegates to the handler's serializeAll()
|
|
|
|
|
for (SyncType type : SyncRegistry.getLoginTypes()) {
|
|
|
|
|
SyncHandler handler = SyncRegistry.getHandler(type);
|
|
|
|
|
if (handler != null)
|
|
|
|
|
registerCache(type, handler::serializeAll);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2026-03-31 22:55:42 +02:00
|
|
|
// Other Login syncers
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2025-11-05 16:52:38 -05:00
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
public static void beginLogin(EntityPlayerMP player) {
|
|
|
|
|
playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new);
|
|
|
|
|
PacketHandler.Instance.sendToPlayer(new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), player);
|
2025-10-19 22:06:11 -04:00
|
|
|
}
|
2025-02-18 22:22:40 -05:00
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
private static void sendPostLoginPackets(EntityPlayerMP player) {
|
|
|
|
|
DBCAddon.instance.syncPlayer(player);
|
|
|
|
|
PlayerDataSyncHandler.getInstance().syncPlayerData(player, false);
|
|
|
|
|
PartyInfoPacket.sendPartyData(player);
|
|
|
|
|
ProfileSharedQuestPacket.sendToPlayer(player);
|
|
|
|
|
|
|
|
|
|
// Sync skin overlays after full handshake to ensure the client is ready.
|
|
|
|
|
// Overlays are not part of getSyncNBTFull(), and packets sent during
|
|
|
|
|
// PlayerLoggedInEvent can arrive before the client entity exists.
|
|
|
|
|
PlayerData data = PlayerData.get(player);
|
|
|
|
|
if (data != null) data.skinOverlays.updateClient();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
// MAIN PIPELINE — completion of login handshake and full catalog sync
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Runs the cached-family sync pass for one player using the currently tracked
|
|
|
|
|
* revision state. Used after the login handshake and by server-side callers
|
|
|
|
|
* that need to refresh a player's global cached sync families.
|
|
|
|
|
*/
|
2026-04-02 00:24:15 +02:00
|
|
|
public static void dispatchSyncForAllTypes(EntityPlayerMP player) {
|
2025-11-05 17:23:21 -05:00
|
|
|
PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new);
|
2025-10-19 22:06:11 -04:00
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// Registry-driven login iteration: iterate only cached types
|
|
|
|
|
for (SyncType type : SyncRegistry.getLoginTypes()) {
|
2025-11-05 17:23:21 -05:00
|
|
|
SyncCacheEntry entry = cacheEntries.get(type);
|
2026-03-31 22:55:42 +02:00
|
|
|
if (entry == null) continue;
|
2025-03-04 18:19:53 -05:00
|
|
|
|
2025-10-19 22:06:11 -04:00
|
|
|
int currentRevision = entry.getRevisionValue();
|
|
|
|
|
int lastRevision = state.getRevision(type);
|
2026-03-31 22:55:42 +02:00
|
|
|
if (lastRevision == currentRevision) continue;
|
2026-03-31 21:44:23 +02:00
|
|
|
|
2025-10-19 22:06:11 -04:00
|
|
|
CachedSyncPayload payload = entry.getPayload(type);
|
2026-03-31 22:55:42 +02:00
|
|
|
if (payload == null) continue;
|
2025-10-19 22:06:11 -04:00
|
|
|
|
|
|
|
|
if (lastRevision != payload.getRevision()) {
|
|
|
|
|
PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player);
|
|
|
|
|
state.updateRevision(type, payload.getRevision());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
sendPostLoginPackets(player);
|
2025-10-19 22:06:11 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
/**
|
|
|
|
|
* Completes the server-side revision handshake that follows {@link LoginPacket}.
|
|
|
|
|
*
|
|
|
|
|
* The client reports which cached revisions it believes are still valid for the
|
|
|
|
|
* current server identity. If the reported server identity is stale or missing,
|
|
|
|
|
* we reset the stored state and force a full cached resync. Otherwise we seed the
|
|
|
|
|
* player's tracked revisions from the client report and only send outdated families.
|
|
|
|
|
*/
|
|
|
|
|
public static void completeLoginRevisionHandshake(EntityPlayerMP player, String serverKey, String previousServerKey,
|
|
|
|
|
Map<SyncType, Integer> clientRevisions) {
|
2025-10-19 22:28:03 -04:00
|
|
|
String currentServerKey = getServerCacheKey();
|
2025-11-05 17:33:24 -05:00
|
|
|
PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new);
|
2025-10-19 22:28:03 -04:00
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
boolean serverIdentityMismatch = !currentServerKey.equals(serverKey);
|
|
|
|
|
boolean previousServerMismatch = !currentServerKey.isEmpty() && (previousServerKey == null || !currentServerKey.equals(
|
|
|
|
|
previousServerKey));
|
|
|
|
|
boolean canReuseClientRevisions = clientRevisions != null && !clientRevisions.isEmpty();
|
2025-10-19 22:06:11 -04:00
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
// Any server identity mismatch means the client's cached revision map is unsafe to reuse.
|
|
|
|
|
if (serverIdentityMismatch || previousServerMismatch || !canReuseClientRevisions)
|
2025-10-21 02:36:34 -04:00
|
|
|
state.reset();
|
2026-03-31 22:55:42 +02:00
|
|
|
else // The client and server agree on identity, so seed the stored state from the report.
|
2025-10-21 02:36:34 -04:00
|
|
|
state.applyHandshake(clientRevisions);
|
2025-10-19 22:06:11 -04:00
|
|
|
|
2026-04-02 00:24:15 +02:00
|
|
|
dispatchSyncForAllTypes(player);
|
2025-10-19 22:06:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2026-03-31 22:55:42 +02:00
|
|
|
// Generic dispatcher — called from outside SyncController
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2025-01-29 18:25:34 -05:00
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
/**
|
|
|
|
|
* Generic full-broadcast helper. Rebuilds the cache for the given type
|
|
|
|
|
* and sends to all connected players.
|
|
|
|
|
*/
|
|
|
|
|
public static void syncAll(SyncType type) {
|
2026-03-31 22:55:42 +02:00
|
|
|
CachedSyncPayload payload = rebuildCache(type);
|
|
|
|
|
if (payload == null) return;
|
|
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
PacketHandler.Instance.sendToAll(new SyncPacket(type, payload));
|
|
|
|
|
updateAllPlayerRevisions(type, payload.getRevision());
|
2026-02-17 23:22:03 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2026-03-31 22:55:42 +02:00
|
|
|
// Server-side dispatchers — registry-driven
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2026-04-02 00:24:15 +02:00
|
|
|
public static void syncUpdate(SyncType type, NBTTagCompound compound) {
|
|
|
|
|
Map<SyncType, Integer> revisions = invalidateCaches(type);
|
|
|
|
|
int revision = getRequestedInvalidationRevision(type, revisions);
|
|
|
|
|
PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, -1, null, revision, compound));
|
|
|
|
|
updateAllPlayerRevisions(revisions);
|
|
|
|
|
}
|
2025-01-29 18:25:34 -05:00
|
|
|
|
2026-04-02 00:24:15 +02:00
|
|
|
public static void syncUpdate(SyncType type, String key, NBTTagCompound compound) {
|
|
|
|
|
Map<SyncType, Integer> revisions = invalidateCaches(type);
|
|
|
|
|
int revision = getRequestedInvalidationRevision(type, revisions);
|
|
|
|
|
PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, -1, key, revision, compound));
|
2025-10-19 22:06:11 -04:00
|
|
|
updateAllPlayerRevisions(revisions);
|
2025-01-29 18:25:34 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
public static void syncUpdate(SyncType type, int cat, NBTTagCompound compound) {
|
|
|
|
|
Map<SyncType, Integer> revisions = invalidateCaches(type);
|
2026-03-31 22:55:42 +02:00
|
|
|
int revision = getRequestedInvalidationRevision(type, revisions);
|
2026-04-02 00:24:15 +02:00
|
|
|
PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, cat, null, revision, compound));
|
2026-03-31 21:44:23 +02:00
|
|
|
updateAllPlayerRevisions(revisions);
|
2025-02-10 02:44:28 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-02 00:24:15 +02:00
|
|
|
public static void syncRemove(SyncType syncType, String key) {
|
|
|
|
|
Map<SyncType, Integer> revisions = invalidateCaches(syncType);
|
|
|
|
|
int revision = getRequestedInvalidationRevision(syncType, revisions);
|
|
|
|
|
PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, -1, key, revision, null));
|
|
|
|
|
updateAllPlayerRevisions(revisions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void syncRemove(SyncType syncType, int id) {
|
|
|
|
|
Map<SyncType, Integer> revisions = invalidateCaches(syncType);
|
|
|
|
|
int revision = getRequestedInvalidationRevision(syncType, revisions);
|
|
|
|
|
PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, id, null, revision, null));
|
|
|
|
|
updateAllPlayerRevisions(revisions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2026-03-31 22:55:42 +02:00
|
|
|
// Client-side handlers — registry-driven handler delegation
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2025-02-10 16:38:50 -05:00
|
|
|
|
2025-01-29 18:25:34 -05:00
|
|
|
@SideOnly(Side.CLIENT)
|
2026-03-31 22:55:42 +02:00
|
|
|
public static void clientHandleAll(SyncType syncType, int revision, NBTTagCompound fullCompound) {
|
2026-03-31 21:44:23 +02:00
|
|
|
SyncHandler handler = SyncRegistry.getHandler(syncType);
|
2026-03-31 22:55:42 +02:00
|
|
|
if (handler == null) return;
|
|
|
|
|
try {
|
2026-03-31 21:44:23 +02:00
|
|
|
handler.clientHandleReload(fullCompound);
|
2026-03-31 22:55:42 +02:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("[SyncController] Failed to handle RELOAD for sync type " + syncType, e);
|
|
|
|
|
}
|
2026-03-31 19:59:53 +02:00
|
|
|
ClientCacheHandler.updateClientRevision(syncType, revision);
|
2026-03-31 22:55:42 +02:00
|
|
|
|
2025-01-29 18:25:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SideOnly(Side.CLIENT)
|
2026-04-02 00:24:15 +02:00
|
|
|
public static void clientHandleUpdate(SyncType syncType, int id, String key, int revision, NBTTagCompound compound) {
|
2026-03-31 21:44:23 +02:00
|
|
|
SyncHandler handler = SyncRegistry.getHandler(syncType);
|
2026-03-31 22:55:42 +02:00
|
|
|
if (handler == null) return;
|
|
|
|
|
try {
|
2026-04-02 00:30:36 +02:00
|
|
|
handler.clientHandleUpdate(compound);
|
2026-04-02 00:24:15 +02:00
|
|
|
handler.clientHandleUpdate(compound, id);
|
|
|
|
|
handler.clientHandleUpdate(compound, key);
|
2026-03-31 22:55:42 +02:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("[SyncController] Failed to handle UPDATE for sync type " + syncType, e);
|
|
|
|
|
}
|
2026-03-31 21:44:23 +02:00
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
ClientCacheHandler.updateClientRevision(syncType, revision);
|
2025-01-29 18:25:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SideOnly(Side.CLIENT)
|
2026-04-02 00:24:15 +02:00
|
|
|
public static void clientHandleRemove(SyncType syncType, int id, String key, int revision) {
|
2026-03-31 21:44:23 +02:00
|
|
|
SyncHandler handler = SyncRegistry.getHandler(syncType);
|
2026-03-31 22:55:42 +02:00
|
|
|
if (handler == null) return;
|
|
|
|
|
try {
|
2026-03-31 21:44:23 +02:00
|
|
|
handler.clientHandleRemove(id);
|
2026-04-02 00:24:15 +02:00
|
|
|
handler.clientHandleRemove(key);
|
2026-03-31 22:55:42 +02:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("[SyncController] Failed to handle REMOVE for sync type " + syncType, e);
|
|
|
|
|
}
|
2026-03-31 21:44:23 +02:00
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
ClientCacheHandler.updateClientRevision(syncType, revision);
|
2025-01-29 18:25:34 -05:00
|
|
|
}
|
2025-02-10 21:41:54 -05:00
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Cache and revision internals
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
2025-10-19 22:06:11 -04:00
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
private static void registerCache(SyncType type, Supplier<NBTTagCompound> supplier) {
|
|
|
|
|
cacheEntries.put(type, new SyncCacheEntry(supplier));
|
2025-10-19 22:06:11 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
private static CachedSyncPayload rebuildCache(SyncType type) {
|
2025-11-05 17:33:24 -05:00
|
|
|
SyncCacheEntry entry = cacheEntries.get(type);
|
2026-03-31 22:55:42 +02:00
|
|
|
if (entry == null) return null;
|
2026-03-31 21:44:23 +02:00
|
|
|
|
2025-10-19 22:06:11 -04:00
|
|
|
entry.invalidate();
|
|
|
|
|
return entry.getPayload(type);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
/**
|
|
|
|
|
* Registry-driven invalidation: delegates to handler-owned
|
|
|
|
|
* invalidation targets via {@link SyncRegistry#getInvalidationTargets}.
|
|
|
|
|
*/
|
2026-03-31 22:55:42 +02:00
|
|
|
private static Map<SyncType, Integer> invalidateCaches(SyncType type) {
|
2026-03-31 21:44:23 +02:00
|
|
|
Set<SyncType> targets = SyncRegistry.getInvalidationTargets(type);
|
2026-03-31 19:59:53 +02:00
|
|
|
Map<SyncType, Integer> revisions = new LinkedHashMap<>();
|
|
|
|
|
for (SyncType target : targets) {
|
2025-11-05 17:33:24 -05:00
|
|
|
SyncCacheEntry entry = cacheEntries.get(target);
|
2025-10-19 22:06:11 -04:00
|
|
|
if (entry != null) {
|
|
|
|
|
int newRevision = entry.invalidate();
|
|
|
|
|
revisions.put(target, newRevision);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return revisions;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
private static int getRequestedInvalidationRevision(SyncType type, Map<SyncType, Integer> revisions) {
|
|
|
|
|
Integer revision = revisions.get(type);
|
|
|
|
|
return revision != null ? revision : getCurrentServerRevision(type);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static int getCurrentServerRevision(SyncType type) {
|
|
|
|
|
SyncCacheEntry entry = cacheEntries.get(type);
|
|
|
|
|
return entry == null ? -1 : entry.getRevisionValue();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static Map<SyncType, Integer> getServerRevisionSnapshot() {
|
|
|
|
|
// Registry-driven: iterate login types from registry
|
|
|
|
|
Map<SyncType, Integer> snapshot = new LinkedHashMap<>();
|
|
|
|
|
for (SyncType type : SyncRegistry.getLoginTypes()) {
|
|
|
|
|
SyncCacheEntry entry = cacheEntries.get(type);
|
|
|
|
|
if (entry != null)
|
|
|
|
|
snapshot.put(type, entry.getRevisionValue());
|
2025-10-19 22:06:11 -04:00
|
|
|
}
|
2026-03-31 22:55:42 +02:00
|
|
|
return snapshot;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void updateAllPlayerRevisions(SyncType type, int revision) {
|
|
|
|
|
if (revision < 0) return;
|
|
|
|
|
|
|
|
|
|
for (PlayerSyncState state : playerSyncState.values())
|
2025-10-19 22:06:11 -04:00
|
|
|
state.updateRevision(type, revision);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
private static void updateAllPlayerRevisions(Map<SyncType, Integer> revisions) {
|
2026-03-31 22:55:42 +02:00
|
|
|
if (revisions.isEmpty()) return;
|
|
|
|
|
|
2025-11-05 17:33:24 -05:00
|
|
|
for (PlayerSyncState state : playerSyncState.values()) {
|
2026-03-31 22:55:42 +02:00
|
|
|
for (Map.Entry<SyncType, Integer> entry : revisions.entrySet())
|
2025-10-19 22:06:11 -04:00
|
|
|
state.updateRevision(entry.getKey(), entry.getValue());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
private static String getServerCacheKey() {
|
|
|
|
|
MinecraftServer server = MinecraftServer.getServer();
|
|
|
|
|
if (server == null || !server.isDedicatedServer()) return "";
|
|
|
|
|
|
|
|
|
|
return SERVER_IDENTITY_KEY;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 21:44:23 +02:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Inner classes — cache payload and player revision state
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2025-10-19 22:06:11 -04:00
|
|
|
public static final class CachedSyncPayload {
|
|
|
|
|
private final int revision;
|
|
|
|
|
private final byte[] payload;
|
|
|
|
|
private final byte[][] chunks;
|
|
|
|
|
|
|
|
|
|
private CachedSyncPayload(int revision, byte[] payload, byte[][] chunks) {
|
|
|
|
|
this.revision = revision;
|
|
|
|
|
this.payload = payload;
|
|
|
|
|
this.chunks = chunks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getRevision() {
|
|
|
|
|
return revision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[] getPayload() {
|
|
|
|
|
return payload;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[][] getChunks() {
|
|
|
|
|
return chunks;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final class SyncCacheEntry {
|
|
|
|
|
private final Supplier<NBTTagCompound> supplier;
|
|
|
|
|
private volatile CachedSyncPayload payload;
|
|
|
|
|
private int revision = 0;
|
|
|
|
|
private boolean dirty = true;
|
|
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
private SyncCacheEntry(Supplier<NBTTagCompound> supplier) {
|
2025-10-19 22:06:11 -04:00
|
|
|
this.supplier = supplier;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
private synchronized CachedSyncPayload getPayload(SyncType requestedType) {
|
2026-03-31 22:55:42 +02:00
|
|
|
if (!dirty && payload != null) return payload;
|
2025-10-19 22:06:11 -04:00
|
|
|
|
|
|
|
|
int payloadRevision = revision;
|
|
|
|
|
ByteBuf buffer = Unpooled.buffer();
|
|
|
|
|
byte[] bytes;
|
|
|
|
|
try {
|
2026-03-31 22:55:42 +02:00
|
|
|
NBTTagCompound data = supplier.get();
|
|
|
|
|
|
2025-10-19 22:06:11 -04:00
|
|
|
buffer.writeInt(requestedType.ordinal());
|
|
|
|
|
buffer.writeInt(EnumSyncAction.RELOAD.ordinal());
|
|
|
|
|
buffer.writeInt(-1);
|
|
|
|
|
buffer.writeInt(payloadRevision);
|
|
|
|
|
ByteBufUtils.writeBigNBT(buffer, data);
|
|
|
|
|
|
|
|
|
|
bytes = new byte[buffer.readableBytes()];
|
|
|
|
|
buffer.readBytes(bytes);
|
2026-03-31 22:55:42 +02:00
|
|
|
} catch (Exception e) {
|
|
|
|
|
LogWriter.error("Failed to serialize sync payload for " + requestedType, e);
|
|
|
|
|
return null;
|
2025-10-19 22:06:11 -04:00
|
|
|
} finally {
|
|
|
|
|
buffer.release();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byte[][] chunks = splitIntoChunks(bytes);
|
|
|
|
|
payload = new CachedSyncPayload(payloadRevision, bytes, chunks);
|
|
|
|
|
dirty = false;
|
|
|
|
|
return payload;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 22:55:42 +02:00
|
|
|
private static byte[][] splitIntoChunks(byte[] payload) {
|
|
|
|
|
int totalSize = payload.length;
|
|
|
|
|
int chunkCount = (totalSize + LargeAbstractPacket.CHUNK_SIZE - 1) / LargeAbstractPacket.CHUNK_SIZE;
|
|
|
|
|
if (chunkCount <= 0)
|
|
|
|
|
chunkCount = 1;
|
|
|
|
|
|
|
|
|
|
byte[][] chunks = new byte[chunkCount][];
|
|
|
|
|
for (int i = 0; i < chunkCount; i++) {
|
|
|
|
|
int offset = i * LargeAbstractPacket.CHUNK_SIZE;
|
|
|
|
|
int length = Math.min(LargeAbstractPacket.CHUNK_SIZE, totalSize - offset);
|
|
|
|
|
byte[] chunk = new byte[length];
|
|
|
|
|
System.arraycopy(payload, offset, chunk, 0, length);
|
|
|
|
|
chunks[i] = chunk;
|
|
|
|
|
}
|
|
|
|
|
return chunks;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 22:06:11 -04:00
|
|
|
private synchronized int invalidate() {
|
|
|
|
|
dirty = true;
|
|
|
|
|
payload = null;
|
|
|
|
|
revision++;
|
|
|
|
|
return revision;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private synchronized int getRevisionValue() {
|
|
|
|
|
return revision;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 16:52:38 -05:00
|
|
|
private synchronized void reset() {
|
|
|
|
|
dirty = true;
|
|
|
|
|
payload = null;
|
|
|
|
|
revision = 0;
|
|
|
|
|
}
|
2025-10-19 22:06:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final class PlayerSyncState {
|
2026-03-31 19:59:53 +02:00
|
|
|
private final Map<SyncType, Integer> revisions = new HashMap<>();
|
2025-10-19 22:06:11 -04:00
|
|
|
|
|
|
|
|
private PlayerSyncState(UUID ignored) {
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
private synchronized int getRevision(SyncType type) {
|
2025-10-19 22:06:11 -04:00
|
|
|
Integer value = revisions.get(type);
|
|
|
|
|
return value == null ? -1 : value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
private synchronized void updateRevision(SyncType type, int revision) {
|
2025-10-19 22:06:11 -04:00
|
|
|
revisions.put(type, revision);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:59:53 +02:00
|
|
|
private synchronized void applyHandshake(Map<SyncType, Integer> incoming) {
|
2026-03-31 22:55:42 +02:00
|
|
|
if (incoming == null || incoming.isEmpty()) return;
|
2025-10-19 22:06:11 -04:00
|
|
|
revisions.putAll(incoming);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-19 22:28:03 -04:00
|
|
|
private synchronized void reset() {
|
|
|
|
|
revisions.clear();
|
|
|
|
|
}
|
2025-10-19 22:06:11 -04:00
|
|
|
}
|
2025-01-29 18:25:34 -05:00
|
|
|
}
|