package kamkeel.npcs.controllers; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.sync.SyncHandler; import kamkeel.npcs.controllers.sync.SyncRegistry; import kamkeel.npcs.controllers.sync.handlers.*; import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; import kamkeel.npcs.network.enums.SyncType; import kamkeel.npcs.network.packets.data.LoginPacket; import kamkeel.npcs.network.packets.data.ProfileSharedQuestPacket; import kamkeel.npcs.network.packets.data.large.SyncPacket; import kamkeel.npcs.network.packets.request.party.PartyInfoPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.server.MinecraftServer; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.controllers.data.PlayerData; import java.io.IOException; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; /** * Central sync backend for CNPC+. * *

This controller manages two distinct sync lanes:

* * *

This class retains ownership of:

* */ public class SyncController { public static boolean DEBUG_SYNC_LOGGING = false; private static final Map cacheEntries = new LinkedHashMap<>(); private static final ConcurrentHashMap playerSyncState = new ConcurrentHashMap<>(); private static void debug(String message, Object... args) { if (DEBUG_SYNC_LOGGING) { System.out.println("[SyncController] " + String.format(message, args)); } } // ═══════════════════════════════════════════════════════════════════════ // Lifecycle — handler registration and cache initialization // ═══════════════════════════════════════════════════════════════════════ /** * Registers all core CNPC+ sync handlers in deterministic order. * Called on server start. The registration order determines login * sync ordering via {@link SyncRegistry#getLoginTypes()}. * *

Handler registration order matches the original hard-coded * LOGIN_SYNC_TYPES array for behavioral parity.

*/ public static void load() { cacheEntries.clear(); playerSyncState.clear(); 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); } SyncRegistry.validateRegistrations(); SyncRegistry.freeze(); } // ═══════════════════════════════════════════════════════════════════════ // Login sync and revision handshake // ═══════════════════════════════════════════════════════════════════════ public static void syncPlayer(EntityPlayerMP player) { SyncController.syncPlayer(player, true); } private static void syncPlayer(EntityPlayerMP player, boolean includePostPackets) { PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); // Registry-driven login iteration: iterate only cached types for (SyncType type : SyncRegistry.getLoginTypes()) { SyncCacheEntry entry = cacheEntries.get(type); if (entry == null) { continue; } int currentRevision = entry.getRevisionValue(); int lastRevision = state.getRevision(type); if (lastRevision == currentRevision) continue; CachedSyncPayload payload = entry.getPayload(type); if (payload == null) continue; if (lastRevision != payload.getRevision()) { debug("Sending %s data to %s", type, player.getCommandSenderName()); PacketHandler.Instance.sendToPlayer(new SyncPacket(type, payload), player); state.updateRevision(type, payload.getRevision()); } } if (includePostPackets) { sendPostLoginPackets(player); } } public static void beginLogin(EntityPlayerMP player) { playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); PacketHandler.Instance.sendToPlayer( new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()), player ); } public static void handleClientRevisionReport( EntityPlayerMP player, String serverKey, String previousServerKey, Map clientRevisions ) { String currentServerKey = getServerCacheKey(); PlayerSyncState state = playerSyncState.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new); if (!currentServerKey.equals(serverKey)) { state.reset(); syncPlayer(player); return; } if (!currentServerKey.isEmpty()) { if (previousServerKey == null || !currentServerKey.equals(previousServerKey)) { state.reset(); syncPlayer(player); return; } } if (clientRevisions == null || clientRevisions.isEmpty()) { debug( "Client %s reported no cached revisions; forcing full sync", player.getCommandSenderName() ); state.reset(); } else { state.applyHandshake(clientRevisions); } syncPlayer(player); } 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(); } } private static Map getServerRevisionSnapshot() { // Registry-driven: iterate login types from registry Map snapshot = new LinkedHashMap<>(); for (SyncType type : SyncRegistry.getLoginTypes()) { SyncCacheEntry entry = cacheEntries.get(type); if (entry != null) snapshot.put(type, entry.getRevisionValue()); } return snapshot; } private static final String SERVER_IDENTITY_KEY = UUID.randomUUID().toString(); private static String getServerCacheKey() { MinecraftServer server = MinecraftServer.getServer(); if (server == null) { return ""; } if (!server.isDedicatedServer()) { return ""; } return SERVER_IDENTITY_KEY; } public static int getCurrentRevision(SyncType type) { SyncCacheEntry entry = cacheEntries.get(type); return entry == null ? -1 : entry.getRevisionValue(); } // ═══════════════════════════════════════════════════════════════════════ // Generic sync dispatch — registry-driven, no family-specific logic // ═══════════════════════════════════════════════════════════════════════ /** * Generic full-broadcast helper. Rebuilds the cache for the given type * and sends to all connected players. */ public static void syncAll(SyncType type) { CachedSyncPayload payload = rebuildNow(type); if (payload == null) { return; } PacketHandler.Instance.sendToAll(new SyncPacket(type, payload)); updateAllPlayerRevisions(type, payload.getRevision()); } // ═══════════════════════════════════════════════════════════════════════ // UPDATE and REMOVE dispatch — generic, registry-driven // ═══════════════════════════════════════════════════════════════════════ public static void syncRemove(SyncType syncType, int id) { Map revisions = invalidateCaches(syncType); int revision = revisions.getOrDefault(syncType, getCurrentRevision(syncType)); PacketHandler.Instance.sendToAll(new SyncPacket(syncType, EnumSyncAction.REMOVE, id, revision, new NBTTagCompound())); updateAllPlayerRevisions(revisions); } public static void syncUpdate(SyncType type, int cat, NBTTagCompound compound) { Map revisions = invalidateCaches(type); int revision = revisions.getOrDefault(type, getCurrentRevision(type)); PacketHandler.Instance.sendToAll(new SyncPacket(type, EnumSyncAction.UPDATE, cat, revision, compound)); updateAllPlayerRevisions(revisions); } // ═══════════════════════════════════════════════════════════════════════ // Client-side dispatch — registry-driven handler delegation // ═══════════════════════════════════════════════════════════════════════ @SideOnly(Side.CLIENT) public static void clientSync(SyncType syncType, int revision, NBTTagCompound fullCompound) { SyncHandler handler = SyncRegistry.getHandler(syncType); if (handler != null) handler.clientHandleReload(fullCompound); ClientCacheHandler.updateClientRevision(syncType, revision); } @SideOnly(Side.CLIENT) public static void clientUpdate(SyncType syncType, int category_id, int revision, NBTTagCompound compound) { SyncHandler handler = SyncRegistry.getHandler(syncType); if (handler != null) handler.clientHandleUpdate(compound, category_id); ClientCacheHandler.updateClientRevision(syncType, revision); } @SideOnly(Side.CLIENT) public static void clientSyncRemove(SyncType syncType, int id, int revision) { SyncHandler handler = SyncRegistry.getHandler(syncType); if (handler != null) handler.clientHandleRemove(id); ClientCacheHandler.updateClientRevision(syncType, revision); } // ═══════════════════════════════════════════════════════════════════════ // Cache and revision internals // ═══════════════════════════════════════════════════════════════════════ private static void registerCache(SyncType type, Supplier supplier) { cacheEntries.put(type, new SyncCacheEntry(supplier)); } private static CachedSyncPayload rebuildNow(SyncType type) { SyncCacheEntry entry = cacheEntries.get(type); if (entry == null) return null; entry.invalidate(); return entry.getPayload(type); } /** * Registry-driven invalidation: delegates to handler-owned * invalidation targets via {@link SyncRegistry#getInvalidationTargets}. */ public static Map invalidateCaches(SyncType type) { Set targets = SyncRegistry.getInvalidationTargets(type); Map revisions = new LinkedHashMap<>(); for (SyncType target : targets) { SyncCacheEntry entry = cacheEntries.get(target); if (entry != null) { int newRevision = entry.invalidate(); revisions.put(target, newRevision); } } return revisions; } private static void updateAllPlayerRevisions(SyncType type, int revision) { if (revision < 0) { return; } for (PlayerSyncState state : playerSyncState.values()) { state.updateRevision(type, revision); } } private static void updateAllPlayerRevisions(Map revisions) { if (revisions.isEmpty()) { return; } for (PlayerSyncState state : playerSyncState.values()) { for (Map.Entry entry : revisions.entrySet()) { state.updateRevision(entry.getKey(), entry.getValue()); } } } // ═══════════════════════════════════════════════════════════════════════ // Inner classes — cache payload and player revision state // ═══════════════════════════════════════════════════════════════════════ 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 supplier; private volatile CachedSyncPayload payload; private int revision = 0; private boolean dirty = true; private SyncCacheEntry(Supplier supplier) { this.supplier = supplier; } private synchronized CachedSyncPayload getPayload(SyncType requestedType) { if (!dirty && payload != null) { return payload; } int payloadRevision = revision; NBTTagCompound data = supplier.get(); ByteBuf buffer = Unpooled.buffer(); byte[] bytes; try { 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); } catch (IOException e) { throw new RuntimeException("Failed to serialize sync payload for " + requestedType, e); } finally { buffer.release(); } byte[][] chunks = splitIntoChunks(bytes); payload = new CachedSyncPayload(payloadRevision, bytes, chunks); dirty = false; return payload; } private synchronized int invalidate() { dirty = true; payload = null; revision++; return revision; } private synchronized int getRevisionValue() { return revision; } private synchronized void reset() { dirty = true; payload = null; revision = 0; } 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; } } private static final class PlayerSyncState { private final Map revisions = new HashMap<>(); private PlayerSyncState(UUID ignored) { } private synchronized int getRevision(SyncType type) { Integer value = revisions.get(type); return value == null ? -1 : value; } private synchronized void updateRevision(SyncType type, int revision) { revisions.put(type, revision); } private synchronized void applyHandshake(Map incoming) { if (incoming == null || incoming.isEmpty()) { return; } revisions.putAll(incoming); } private synchronized void reset() { revisions.clear(); } } }