Merge pull request #220 from KAMKEEL/codex/optimize-player-sync-performance-0c9js2

Add debug logging toggle for sync controller
This commit is contained in:
Kam authored and GitHub committed 2025-10-27 14:59:43 -04:00
commit faf1019ed1
24 files changed
+715 -124

No files matched your search

@@ -3,6 +3,9 @@ package kamkeel.npcs.controllers;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.addon.DBCAddon;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import kamkeel.npcs.network.LargeAbstractPacket;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumSyncAction;
import kamkeel.npcs.network.enums.EnumSyncType;
@@ -10,10 +13,12 @@ import kamkeel.npcs.network.packets.data.LoginPacket;
import kamkeel.npcs.network.packets.data.large.SyncEffectPacket;
import kamkeel.npcs.network.packets.data.large.SyncPacket;
import kamkeel.npcs.network.packets.request.party.PartyInfoPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.server.MinecraftServer;
import noppes.npcs.client.ClientCacheHandler;
import noppes.npcs.controllers.CustomEffectController;
import noppes.npcs.controllers.DialogController;
@@ -35,91 +40,169 @@ import noppes.npcs.controllers.data.QuestCategory;
import noppes.npcs.controllers.data.RecipeAnvil;
import noppes.npcs.controllers.data.RecipeCarpentry;
import java.io.IOException;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
public class SyncController {
public static boolean DEBUG_SYNC_LOGGING = false;
private static void debug(String message, Object... args) {
if (DEBUG_SYNC_LOGGING) {
System.out.println("[SyncController] " + String.format(message, args));
}
}
private static final EnumSyncType[] LOGIN_SYNC_TYPES = new EnumSyncType[]{
EnumSyncType.FACTION,
EnumSyncType.DIALOG_CATEGORY,
EnumSyncType.QUEST_CATEGORY,
EnumSyncType.WORKBENCH_RECIPES,
EnumSyncType.CARPENTRY_RECIPES,
EnumSyncType.ANVIL_RECIPES,
EnumSyncType.CUSTOM_EFFECTS,
EnumSyncType.MAGIC,
EnumSyncType.MAGIC_CYCLE
};
private static final EnumMap<EnumSyncType, SyncCacheEntry> CACHE_ENTRIES = new EnumMap<>(EnumSyncType.class);
private static final ConcurrentHashMap<UUID, PlayerSyncState> PLAYER_SYNC_STATE = new ConcurrentHashMap<>();
static {
registerCache(EnumSyncType.FACTION, SyncController::factionsNBT);
registerCache(EnumSyncType.DIALOG_CATEGORY, SyncController::dialogCategoriesNBT);
registerCache(EnumSyncType.QUEST_CATEGORY, SyncController::questCategoriesNBT);
registerCache(EnumSyncType.WORKBENCH_RECIPES, SyncController::workbenchNBT);
registerCache(EnumSyncType.CARPENTRY_RECIPES, SyncController::carpentryNBT);
registerCache(EnumSyncType.ANVIL_RECIPES, SyncController::anvilNBT);
registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT);
registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT);
registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT);
}
public static void syncPlayer(EntityPlayerMP player) {
syncPlayer(player, true);
}
// Login Packet
PacketHandler.Instance.sendToPlayer(new LoginPacket(), player);
private static void syncPlayer(EntityPlayerMP player, boolean includePostPackets) {
PlayerSyncState state = PLAYER_SYNC_STATE.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new);
// 1) Factions
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.FACTION,
EnumSyncAction.RELOAD,
-1,
factionsNBT()
), player);
for (EnumSyncType type : LOGIN_SYNC_TYPES) {
SyncCacheEntry entry = CACHE_ENTRIES.get(type);
if (entry == null) {
continue;
}
// 2) Dialog Categories
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.DIALOG_CATEGORY,
EnumSyncAction.RELOAD,
-1,
dialogCategoriesNBT()
), player);
int currentRevision = entry.getRevisionValue();
int lastRevision = state.getRevision(type);
if (lastRevision == currentRevision) {
continue;
}
// 3) Quest Categories
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.QUEST_CATEGORY,
EnumSyncAction.RELOAD,
-1,
questCategoriesNBT()
), player);
CachedSyncPayload payload = entry.getPayload(type);
if (payload == null) {
continue;
}
// 4) Workbench Recipes
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.WORKBENCH_RECIPES,
EnumSyncAction.RELOAD,
-1,
workbenchNBT()
), player);
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());
}
}
// 5) Carpentry Recipes
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.CARPENTRY_RECIPES,
EnumSyncAction.RELOAD,
-1,
carpentryNBT()
), player);
if (includePostPackets) {
sendPostLoginPackets(player);
}
}
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.ANVIL_RECIPES,
EnumSyncAction.RELOAD,
-1,
anvilNBT()
), player);
public static void beginLogin(EntityPlayerMP player) {
PLAYER_SYNC_STATE.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new);
PacketHandler.Instance.sendToPlayer(
new LoginPacket(getServerCacheKey(), getServerRevisionSnapshot()),
player
);
}
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.CUSTOM_EFFECTS,
EnumSyncAction.RELOAD,
-1,
customEffectsNBT()
), player);
public static void handleClientRevisionReport(
EntityPlayerMP player,
String serverKey,
String previousServerKey,
Map<EnumSyncType, Integer> clientRevisions
) {
String currentServerKey = getServerCacheKey();
PlayerSyncState state = PLAYER_SYNC_STATE.computeIfAbsent(player.getUniqueID(), PlayerSyncState::new);
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.MAGIC,
EnumSyncAction.RELOAD,
-1,
magicsNBT()
), player);
if (!currentServerKey.equals(serverKey)) {
state.reset();
syncPlayer(player);
return;
}
PacketHandler.Instance.sendToPlayer(new SyncPacket(
EnumSyncType.MAGIC_CYCLE,
EnumSyncAction.RELOAD,
-1,
magicCyclesNBT()
), player);
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);
syncPlayerData(player, false);
// Send party information to the player on login
PartyInfoPacket.sendPartyData(player);
}
private static EnumMap<EnumSyncType, Integer> getServerRevisionSnapshot() {
EnumMap<EnumSyncType, Integer> snapshot = new EnumMap<>(EnumSyncType.class);
for (EnumSyncType type : LOGIN_SYNC_TYPES) {
SyncCacheEntry entry = CACHE_ENTRIES.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(EnumSyncType type) {
SyncCacheEntry entry = CACHE_ENTRIES.get(type);
return entry == null ? -1 : entry.getRevisionValue();
}
public static NBTTagCompound workbenchNBT() {
RecipeController controller = RecipeController.Instance;
NBTTagList list = new NBTTagList();
@@ -257,70 +340,74 @@ public class SyncController {
}
public static void syncRemove(EnumSyncType enumSyncType, int id) {
Map<EnumSyncType, Integer> revisions = invalidateCaches(enumSyncType);
int revision = revisions.getOrDefault(enumSyncType, getCurrentRevision(enumSyncType));
PacketHandler.Instance.sendToAll(new SyncPacket(
enumSyncType,
EnumSyncAction.REMOVE,
id,
revision,
new NBTTagCompound()
));
updateAllPlayerRevisions(revisions);
}
public static void syncAllDialogs() {
PacketHandler.Instance.sendToAll(new SyncPacket(
EnumSyncType.DIALOG_CATEGORY,
EnumSyncAction.RELOAD,
-1,
dialogCategoriesNBT()
));
CachedSyncPayload payload = rebuildNow(EnumSyncType.DIALOG_CATEGORY);
if (payload == null) {
return;
}
PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.DIALOG_CATEGORY, payload));
updateAllPlayerRevisions(EnumSyncType.DIALOG_CATEGORY, payload.getRevision());
}
public static void syncAllQuests() {
PacketHandler.Instance.sendToAll(new SyncPacket(
EnumSyncType.QUEST_CATEGORY,
EnumSyncAction.RELOAD,
-1,
questCategoriesNBT()
));
CachedSyncPayload payload = rebuildNow(EnumSyncType.QUEST_CATEGORY);
if (payload == null) {
return;
}
PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.QUEST_CATEGORY, payload));
updateAllPlayerRevisions(EnumSyncType.QUEST_CATEGORY, payload.getRevision());
}
public static void syncAllWorkbenchRecipes() {
PacketHandler.Instance.sendToAll(new SyncPacket(
EnumSyncType.WORKBENCH_RECIPES,
EnumSyncAction.RELOAD,
-1,
workbenchNBT()
));
CachedSyncPayload payload = rebuildNow(EnumSyncType.WORKBENCH_RECIPES);
if (payload == null) {
return;
}
PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.WORKBENCH_RECIPES, payload));
updateAllPlayerRevisions(EnumSyncType.WORKBENCH_RECIPES, payload.getRevision());
}
public static void syncAllCarpentryRecipes() {
PacketHandler.Instance.sendToAll(new SyncPacket(
EnumSyncType.CARPENTRY_RECIPES,
EnumSyncAction.RELOAD,
-1,
carpentryNBT()
));
CachedSyncPayload payload = rebuildNow(EnumSyncType.CARPENTRY_RECIPES);
if (payload == null) {
return;
}
PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CARPENTRY_RECIPES, payload));
updateAllPlayerRevisions(EnumSyncType.CARPENTRY_RECIPES, payload.getRevision());
}
public static void syncAllAnvilRecipes() {
PacketHandler.Instance.sendToAll(new SyncPacket(
EnumSyncType.ANVIL_RECIPES,
EnumSyncAction.RELOAD,
-1,
anvilNBT()
));
CachedSyncPayload payload = rebuildNow(EnumSyncType.ANVIL_RECIPES);
if (payload == null) {
return;
}
PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.ANVIL_RECIPES, payload));
updateAllPlayerRevisions(EnumSyncType.ANVIL_RECIPES, payload.getRevision());
}
public static void syncAllCustomEffects() {
PacketHandler.Instance.sendToAll(new SyncPacket(
EnumSyncType.CUSTOM_EFFECTS,
EnumSyncAction.RELOAD,
-1,
customEffectsNBT()
));
CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_EFFECTS);
if (payload == null) {
return;
}
PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_EFFECTS, payload));
updateAllPlayerRevisions(EnumSyncType.CUSTOM_EFFECTS, payload.getRevision());
}
@SideOnly(Side.CLIENT)
public static void clientSync(EnumSyncType enumSyncType, NBTTagCompound fullCompound) {
public static void clientSync(EnumSyncType enumSyncType, int revision, NBTTagCompound fullCompound) {
switch (enumSyncType) {
case FACTION: {
NBTTagList list = fullCompound.getTagList("Factions", 10);
@@ -505,13 +592,15 @@ public class SyncController {
break;
}
}
ClientCacheHandler.updateClientRevision(enumSyncType, revision);
}
/**
* Update Related Tasks
*/
@SideOnly(Side.CLIENT)
public static void clientUpdate(EnumSyncType enumSyncType, int category_id, NBTTagCompound compound) {
public static void clientUpdate(EnumSyncType enumSyncType, int category_id, int revision, NBTTagCompound compound) {
switch (enumSyncType) {
case FACTION: {
Faction faction = new Faction();
@@ -596,15 +685,21 @@ public class SyncController {
break;
}
}
ClientCacheHandler.updateClientRevision(enumSyncType, revision);
}
public static void syncUpdate(EnumSyncType type, int cat, NBTTagCompound compound) {
Map<EnumSyncType, Integer> revisions = invalidateCaches(type);
int revision = revisions.getOrDefault(type, getCurrentRevision(type));
PacketHandler.Instance.sendToAll(new SyncPacket(
type,
EnumSyncAction.UPDATE,
cat,
revision,
compound
));
updateAllPlayerRevisions(revisions);
}
@@ -633,7 +728,7 @@ public class SyncController {
}
@SideOnly(Side.CLIENT)
public static void clientSyncRemove(EnumSyncType enumSyncType, int id) {
public static void clientSyncRemove(EnumSyncType enumSyncType, int id, int revision) {
switch (enumSyncType) {
case FACTION:
FactionController.getInstance().factions.remove(id);
@@ -675,6 +770,8 @@ public class SyncController {
CustomEffectController.Instance.getCustomEffects().remove(id);
break;
}
ClientCacheHandler.updateClientRevision(enumSyncType, revision);
}
public static void syncEffects(EntityPlayerMP playerMP) {
@@ -693,4 +790,198 @@ public class SyncController {
playerData.setPlayerEffects(compound);
}
}
private static void registerCache(EnumSyncType type, Supplier<NBTTagCompound> supplier) {
CACHE_ENTRIES.put(type, new SyncCacheEntry(type, supplier));
}
private static CachedSyncPayload rebuildNow(EnumSyncType type) {
SyncCacheEntry entry = CACHE_ENTRIES.get(type);
if (entry == null) {
return null;
}
entry.invalidate();
return entry.getPayload(type);
}
public static Map<EnumSyncType, Integer> invalidateCaches(EnumSyncType type) {
EnumSet<EnumSyncType> targets = getInvalidationTargets(type);
EnumMap<EnumSyncType, Integer> revisions = new EnumMap<>(EnumSyncType.class);
for (EnumSyncType target : targets) {
SyncCacheEntry entry = CACHE_ENTRIES.get(target);
if (entry != null) {
int newRevision = entry.invalidate();
revisions.put(target, newRevision);
}
}
return revisions;
}
private static EnumSet<EnumSyncType> getInvalidationTargets(EnumSyncType type) {
switch (type) {
case DIALOG:
case DIALOG_CATEGORY:
return EnumSet.of(EnumSyncType.DIALOG_CATEGORY);
case QUEST:
case QUEST_CATEGORY:
return EnumSet.of(EnumSyncType.QUEST_CATEGORY);
case FACTION:
return EnumSet.of(EnumSyncType.FACTION);
case MAGIC:
return EnumSet.of(EnumSyncType.MAGIC);
case MAGIC_CYCLE:
return EnumSet.of(EnumSyncType.MAGIC_CYCLE);
case WORKBENCH_RECIPES:
return EnumSet.of(EnumSyncType.WORKBENCH_RECIPES);
case CARPENTRY_RECIPES:
return EnumSet.of(EnumSyncType.CARPENTRY_RECIPES);
case ANVIL_RECIPES:
return EnumSet.of(EnumSyncType.ANVIL_RECIPES);
case CUSTOM_EFFECTS:
return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS);
default:
return EnumSet.noneOf(EnumSyncType.class);
}
}
private static void updateAllPlayerRevisions(EnumSyncType type, int revision) {
if (revision < 0) {
return;
}
for (PlayerSyncState state : PLAYER_SYNC_STATE.values()) {
state.updateRevision(type, revision);
}
}
private static void updateAllPlayerRevisions(Map<EnumSyncType, Integer> revisions) {
if (revisions.isEmpty()) {
return;
}
for (PlayerSyncState state : PLAYER_SYNC_STATE.values()) {
for (Map.Entry<EnumSyncType, Integer> entry : revisions.entrySet()) {
state.updateRevision(entry.getKey(), entry.getValue());
}
}
}
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;
private SyncCacheEntry(EnumSyncType type, Supplier<NBTTagCompound> supplier) {
this.supplier = supplier;
}
private synchronized CachedSyncPayload getPayload(EnumSyncType 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 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 EnumMap<EnumSyncType, Integer> revisions = new EnumMap<>(EnumSyncType.class);
private PlayerSyncState(UUID ignored) {
}
private synchronized int getRevision(EnumSyncType type) {
Integer value = revisions.get(type);
return value == null ? -1 : value;
}
private synchronized void updateRevision(EnumSyncType type, int revision) {
revisions.put(type, revision);
}
private synchronized void applyHandshake(Map<EnumSyncType, Integer> incoming) {
if (incoming == null || incoming.isEmpty()) {
return;
}
revisions.putAll(incoming);
}
private synchronized void reset() {
revisions.clear();
}
}
}
@@ -16,7 +16,7 @@ import java.util.concurrent.ConcurrentHashMap;
public abstract class LargeAbstractPacket extends AbstractPacket {
private static final Map<UUID, PacketStorage> packetChunks = new ConcurrentHashMap<>();
private static final int CHUNK_SIZE = 10000;
public static final int CHUNK_SIZE = 10000;
/**
* Create multiple FMLProxyPacket objects, each containing up to CHUNK_SIZE bytes.
@@ -71,6 +71,7 @@ import kamkeel.npcs.network.packets.player.item.MagicCyclesPacket;
import kamkeel.npcs.network.packets.player.profile.ProfileChangePacket;
import kamkeel.npcs.network.packets.player.profile.ProfileCreatePacket;
import kamkeel.npcs.network.packets.player.profile.ProfileGetInfoPacket;
import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket;
import kamkeel.npcs.network.packets.player.profile.ProfileGetPacket;
import kamkeel.npcs.network.packets.player.profile.ProfileRemovePacket;
import kamkeel.npcs.network.packets.player.profile.ProfileRenamePacket;
@@ -575,6 +576,8 @@ public class PacketHandler {
PLAYER_PACKET.registerPacket(new ScreenSizePacket());
PLAYER_PACKET.registerPacket(new SyncRevisionInfoPacket());
// CustomGUI Packets
PLAYER_PACKET.registerPacket(new CustomGuiButtonPacket());
PLAYER_PACKET.registerPacket(new CustomGuiClosePacket());
@@ -28,5 +28,8 @@ public enum EnumPlayerPacket {
ProfileCreate,
ProfileRemove,
ProfileRename,
ProfileChange, ProfileGet, ProfileGetInfo,
ProfileChange,
ProfileGet,
ProfileGetInfo,
SyncRevisionInfo,
}
@@ -5,20 +5,38 @@ import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumDataPacket;
import kamkeel.npcs.network.enums.EnumSyncType;
import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import noppes.npcs.client.ClientCacheHandler;
import noppes.npcs.config.ConfigMain;
import java.io.IOException;
import java.util.EnumMap;
import java.util.Map;
public final class LoginPacket extends AbstractPacket {
public static final String packetName = "CNPC+|Login";
private String serverCacheKey = "";
private final EnumMap<EnumSyncType, Integer> revisionSnapshot = new EnumMap<>(EnumSyncType.class);
public LoginPacket() {
}
public LoginPacket(String serverCacheKey, EnumMap<EnumSyncType, Integer> revisionSnapshot) {
if (serverCacheKey != null) {
this.serverCacheKey = serverCacheKey;
}
if (revisionSnapshot != null) {
this.revisionSnapshot.putAll(revisionSnapshot);
}
}
@Override
public Enum getType() {
return EnumDataPacket.LOGIN;
@@ -33,6 +51,12 @@ public final class LoginPacket extends AbstractPacket {
public void sendData(ByteBuf out) throws IOException {
out.writeBoolean(ConfigMain.PartiesEnabled);
out.writeBoolean(ConfigMain.ProfilesEnabled);
ByteBufUtils.writeString(out, serverCacheKey);
out.writeShort(revisionSnapshot.size());
for (Map.Entry<EnumSyncType, Integer> entry : revisionSnapshot.entrySet()) {
out.writeInt(entry.getKey().ordinal());
out.writeInt(entry.getValue());
}
}
@SideOnly(Side.CLIENT)
@@ -40,5 +64,22 @@ public final class LoginPacket extends AbstractPacket {
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
ClientCacheHandler.allowParties = in.readBoolean();
ClientCacheHandler.allowProfiles = in.readBoolean();
String serverKey = ByteBufUtils.readString(in);
int revisionCount = in.readUnsignedShort();
EnumMap<EnumSyncType, Integer> serverRevisions = new EnumMap<>(EnumSyncType.class);
for (int i = 0; i < revisionCount; i++) {
EnumSyncType type = EnumSyncType.values()[in.readInt()];
int revision = in.readInt();
serverRevisions.put(type, revision);
}
ClientCacheHandler.setActiveServer(serverKey, serverRevisions);
PacketClient.sendClient(new SyncRevisionInfoPacket(
serverKey,
ClientCacheHandler.getLastServerKey(),
ClientCacheHandler.getCachedRevisionsForServer(serverKey)
));
}
}
@@ -1,5 +1,6 @@
package kamkeel.npcs.network.packets.data.large;
import cpw.mods.fml.common.network.internal.FMLProxyPacket;
import cpw.mods.fml.relauncher.Side;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -17,6 +18,9 @@ import noppes.npcs.CustomNpcs;
import noppes.npcs.LogWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* A large sync packet that sends chunked data to the client for a given SyncType
@@ -28,6 +32,9 @@ public final class SyncPacket extends LargeAbstractPacket {
private EnumSyncAction enumSyncAction;
private NBTTagCompound syncData;
private int operationID;
private int revision = -1;
private byte[] cachedPayload;
private byte[][] cachedChunks;
public SyncPacket() {
}
@@ -36,10 +43,24 @@ public final class SyncPacket extends LargeAbstractPacket {
* Constructs a new LargeSyncPacket.
*/
public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, NBTTagCompound syncData) {
this(enumSyncType, enumSyncAction, catId, -1, syncData);
}
public SyncPacket(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int catId, int revision, NBTTagCompound syncData) {
this.enumSyncType = enumSyncType;
this.enumSyncAction = enumSyncAction;
this.syncData = syncData;
this.operationID = catId;
this.revision = revision;
}
public SyncPacket(EnumSyncType enumSyncType, SyncController.CachedSyncPayload payload) {
this.enumSyncType = enumSyncType;
this.enumSyncAction = EnumSyncAction.RELOAD;
this.operationID = -1;
this.revision = payload.getRevision();
this.cachedPayload = payload.getPayload();
this.cachedChunks = payload.getChunks();
}
@Override
@@ -54,20 +75,55 @@ public final class SyncPacket extends LargeAbstractPacket {
@Override
protected byte[] getData() throws IOException {
ByteBuf buffer = Unpooled.buffer();
// 1) Write SyncType
buffer.writeInt(enumSyncType.ordinal());
// 2) Write SyncAction
buffer.writeInt(enumSyncAction.ordinal());
// 3) Optional Category ID
buffer.writeInt(operationID);
// 4) Write the NBTTagCompound
ByteBufUtils.writeBigNBT(buffer, syncData);
if (cachedPayload != null) {
return cachedPayload;
}
// Copy the buffer’s readable bytes into a byte[]
byte[] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
return bytes;
ByteBuf buffer = Unpooled.buffer();
try {
buffer.writeInt(enumSyncType.ordinal());
buffer.writeInt(enumSyncAction.ordinal());
buffer.writeInt(operationID);
buffer.writeInt(revision);
ByteBufUtils.writeBigNBT(buffer, syncData);
byte[] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
return bytes;
} finally {
buffer.release();
}
}
@Override
public List<FMLProxyPacket> generatePackets() {
if (cachedChunks == null) {
return super.generatePackets();
}
List<FMLProxyPacket> packets = new ArrayList<>(cachedChunks.length);
PacketChannel packetChannel = getChannel();
EnumDataPacket dataPacket = (EnumDataPacket) getType();
UUID packetId = UUID.randomUUID();
int totalSize = cachedPayload.length;
int offset = 0;
for (byte[] chunk : cachedChunks) {
ByteBuf chunkBuf = Unpooled.buffer();
chunkBuf.writeInt(packetChannel.getChannelType().ordinal());
chunkBuf.writeInt(dataPacket.ordinal());
chunkBuf.writeLong(packetId.getMostSignificantBits());
chunkBuf.writeLong(packetId.getLeastSignificantBits());
chunkBuf.writeInt(totalSize);
chunkBuf.writeInt(offset);
chunkBuf.writeInt(cachedChunks.length);
chunkBuf.writeBytes(chunk);
packets.add(new FMLProxyPacket(chunkBuf, packetChannel.getChannelName()));
offset += chunk.length;
}
return packets;
}
@Override
@@ -79,28 +135,35 @@ public final class SyncPacket extends LargeAbstractPacket {
int syncTypeOrdinal = data.readInt();
int syncActionOrdinal = data.readInt();
int categoryID = data.readInt();
int incomingRevision = data.readInt();
EnumSyncType type = EnumSyncType.values()[syncTypeOrdinal];
EnumSyncAction action = EnumSyncAction.values()[syncActionOrdinal];
try {
NBTTagCompound tag = ByteBufUtils.readBigNBT(data);
// Now do your client-side logic (similar to your old clientSync() or clientSyncUpdate() approach)
handleSyncPacketClient(type, action, categoryID, tag);
handleSyncPacketClient(type, action, categoryID, incomingRevision, tag);
} catch (RuntimeException e) {
LogWriter.error(String.format("Attempted to Sync %s but it was too big", type.toString()));
}
}
private void handleSyncPacketClient(EnumSyncType enumSyncType, EnumSyncAction enumSyncAction, int id, NBTTagCompound data) {
private void handleSyncPacketClient(
EnumSyncType enumSyncType,
EnumSyncAction enumSyncAction,
int id,
int incomingRevision,
NBTTagCompound data
) {
switch (enumSyncAction) {
case RELOAD:
SyncController.clientSync(enumSyncType, data);
SyncController.clientSync(enumSyncType, incomingRevision, data);
break;
case UPDATE:
SyncController.clientUpdate(enumSyncType, id, data);
SyncController.clientUpdate(enumSyncType, id, incomingRevision, data);
break;
case REMOVE:
SyncController.clientSyncRemove(enumSyncType, id);
SyncController.clientSyncRemove(enumSyncType, id, incomingRevision);
break;
}
}
@@ -0,0 +1,83 @@
package kamkeel.npcs.network.packets.player;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumPlayerPacket;
import kamkeel.npcs.network.enums.EnumSyncType;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import java.io.IOException;
import java.util.EnumMap;
import java.util.Map;
public class SyncRevisionInfoPacket extends AbstractPacket {
public static final String packetName = "Player|SyncRevisionInfo";
private String serverKey = "";
private String previousServerKey = "";
private final EnumMap<EnumSyncType, Integer> revisions = new EnumMap<>(EnumSyncType.class);
public SyncRevisionInfoPacket() {
}
public SyncRevisionInfoPacket(String serverKey, String previousServerKey, EnumMap<EnumSyncType, Integer> revisions) {
if (serverKey != null) {
this.serverKey = serverKey;
}
if (previousServerKey != null) {
this.previousServerKey = previousServerKey;
}
if (revisions != null) {
this.revisions.putAll(revisions);
}
}
@Override
public Enum getType() {
return EnumPlayerPacket.SyncRevisionInfo;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.PLAYER_PACKET;
}
@Override
@SideOnly(Side.CLIENT)
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeString(out, serverKey);
ByteBufUtils.writeString(out, previousServerKey);
out.writeShort(revisions.size());
for (Map.Entry<EnumSyncType, Integer> entry : revisions.entrySet()) {
out.writeInt(entry.getKey().ordinal());
out.writeInt(entry.getValue());
}
}
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
if (!(player instanceof EntityPlayerMP)) {
return;
}
String incomingServerKey = ByteBufUtils.readString(in);
String incomingPreviousKey = ByteBufUtils.readString(in);
int revisionCount = in.readUnsignedShort();
EnumMap<EnumSyncType, Integer> incomingRevisions = new EnumMap<>(EnumSyncType.class);
for (int i = 0; i < revisionCount; i++) {
EnumSyncType type = EnumSyncType.values()[in.readInt()];
int revision = in.readInt();
incomingRevisions.put(type, revision);
}
SyncController.handleClientRevisionReport((EntityPlayerMP) player, incomingServerKey, incomingPreviousKey, incomingRevisions);
}
}
@@ -209,6 +209,7 @@ public class ServerEventsHandler {
EnumSyncType.WORKBENCH_RECIPES,
EnumSyncAction.RELOAD,
-1,
SyncController.getCurrentRevision(EnumSyncType.WORKBENCH_RECIPES),
SyncController.workbenchNBT()
), (EntityPlayerMP) player);
}
@@ -217,6 +218,7 @@ public class ServerEventsHandler {
EnumSyncType.CARPENTRY_RECIPES,
EnumSyncAction.RELOAD,
-1,
SyncController.getCurrentRevision(EnumSyncType.CARPENTRY_RECIPES),
SyncController.carpentryNBT()
), (EntityPlayerMP) player);
}
@@ -62,7 +62,7 @@ public class ServerTickHandler {
}
ProfileController.Instance.login(player);
SyncController.syncPlayer(player);
SyncController.beginLogin(player);
SyncController.syncEffects(player);
}
@@ -15,9 +15,12 @@ import noppes.npcs.controllers.data.AnimationData;
import noppes.npcs.controllers.data.Party;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.controllers.data.SkinOverlay;
import kamkeel.npcs.network.enums.EnumSyncType;
import noppes.npcs.util.CacheHashMap;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class ClientCacheHandler {
@@ -27,11 +30,70 @@ public class ClientCacheHandler {
public static HashMap<UUID, HashMap<Integer, SkinOverlay>> skinOverlays = new HashMap<>();
public static HashMap<UUID, AnimationData> playerAnimations = new HashMap<>();
private static String activeServerKey = "";
private static String lastServerKey = "";
private static final Map<String, EnumMap<EnumSyncType, Integer>> clientRevisionCache = new HashMap<>();
public static Party party;
public static boolean allowProfiles = true;
public static boolean allowParties = true;
public static void setActiveServer(String serverKey, EnumMap<EnumSyncType, Integer> serverRevisions) {
String normalizedKey = serverKey == null ? "" : serverKey;
String previousKey = activeServerKey == null ? "" : activeServerKey;
lastServerKey = previousKey;
if (!normalizedKey.equals(activeServerKey)) {
clientRevisionCache.clear();
}
activeServerKey = normalizedKey;
if (activeServerKey.isEmpty()) {
return;
}
EnumMap<EnumSyncType, Integer> cached = clientRevisionCache.computeIfAbsent(
activeServerKey,
ignored -> new EnumMap<>(EnumSyncType.class)
);
if (serverRevisions != null && !serverRevisions.isEmpty()) {
cached.keySet().retainAll(serverRevisions.keySet());
}
}
public static String getLastServerKey() {
return lastServerKey == null ? "" : lastServerKey;
}
public static EnumMap<EnumSyncType, Integer> getCachedRevisionsForServer(String serverKey) {
String key = serverKey == null ? "" : serverKey;
if (key.isEmpty()) {
return new EnumMap<>(EnumSyncType.class);
}
EnumMap<EnumSyncType, Integer> revisions = clientRevisionCache.get(key);
if (revisions == null) {
return new EnumMap<>(EnumSyncType.class);
}
return new EnumMap<>(revisions);
}
public static void updateClientRevision(EnumSyncType type, int revision) {
if (revision < 0) {
return;
}
String key = activeServerKey == null ? "" : activeServerKey;
if (key.isEmpty()) {
return;
}
EnumMap<EnumSyncType, Integer> revisions = clientRevisionCache.computeIfAbsent(
key,
ignored -> new EnumMap<>(EnumSyncType.class)
);
revisions.put(type, revision);
}
public static ImageData getImageData(String directory) {
synchronized (imageDataCache) {
@@ -41,6 +41,8 @@ public class GuiMagicCycleMap extends GuiDiagram {
invalidateCache();
return;
}
// TODO: Consumes MagicController's full magic registry to resolve associations; visible only through magic editing GUIs
// that require the NPC wand and CustomNpcsPermissions.GLOBAL_MAGIC (or op status) to open.
MagicController mc = MagicController.getInstance();
// Get magics from the cycle's associations if associations are not null.
if (cycle.associations != null) {
@@ -67,6 +69,8 @@ public class GuiMagicCycleMap extends GuiDiagram {
* Constructor using a cycle ID.
*/
public GuiMagicCycleMap(GuiNPCInterface parent, int x, int y, int width, int height, int cycleId) {
// TODO: Looks up cycle data from MagicController; accessed from the same wand/CustomNpcsPermissions.GLOBAL_MAGIC gated
// management screens.
this(parent, x, y, width, height, MagicController.getInstance().getCycle(cycleId));
}
@@ -236,6 +236,8 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall
initGui();
}
if (guibutton.id == 106) {
// TODO: Uses ScriptController.Instance.dir (shared script repository); this GUI is gated behind the scripter tool
// and CustomNpcsPermissions.TOOL_SCRIPTER so only authorized editors touch the controller-backed scripts.
NoppesUtil.openFolder(ScriptController.Instance.dir);
}
if (guibutton.id == 107) {
@@ -19,6 +19,8 @@ public class SubGuiMagicCycleViewer extends SubGuiInterface {
}
public SubGuiMagicCycleViewer(int cycleID) {
// TODO: Fetches MagicController cycle definitions (global magic data); only staff with the wand and
// CustomNpcsPermissions.GLOBAL_MAGIC reach this viewer via the magic management GUIs.
this(MagicController.getInstance().getCycle(cycleID));
}
@@ -124,6 +124,8 @@ public class GuiNPCManageEffects extends GuiNPCInterface2 implements ICustomScro
effect.readFromNBT(compound);
setSelected(effect.name);
if (effect.id != -1) {
// TODO: Writes back into the CustomEffectController cache (global effect dataset); only players with the wand and
// CustomNpcsPermissions.GLOBAL_EFFECT reach this admin editor.
CustomEffectController.getInstance().getCustomEffects().replace(effect.id, effect);
}
initGui();
@@ -41,6 +41,8 @@ public class GuiAnvilRecipes extends GuiNPCInterface {
setBackground("recipesAnvil.png");
this.closeOnEsc = true;
// TODO: Loads the entire anvil recipe registry from RecipeController; any player can open this reference GUI without
// special permissions.
recipes.addAll(RecipeController.Instance.anvilRecipes.values());
}
@@ -81,9 +83,12 @@ public class GuiAnvilRecipes extends GuiNPCInterface {
private List<RecipeAnvil> getSearchList() {
if (search.isEmpty()) {
// TODO: Returns every anvil recipe from RecipeController for browsing; no CustomNpcsPermissions gate applies.
return new ArrayList<RecipeAnvil>(RecipeController.Instance.anvilRecipes.values());
}
List<RecipeAnvil> list = new ArrayList<RecipeAnvil>();
// TODO: Filters across the RecipeController anvil collection; the bench/anvil GUIs are normal gameplay with no
// permission checks.
for (RecipeAnvil recipe : RecipeController.Instance.anvilRecipes.values()) {
ItemStack repairItem = recipe.itemToRepair;
if (repairItem == null)
@@ -39,6 +39,8 @@ public class GuiNpcAnvil extends GuiContainerNPCInterface {
@Override
protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) {
// Enable the button if there are any anvil recipes.
// TODO: Checks the shared RecipeController anvil list (entire dataset) before enabling; this player crafting GUI is
// public, so no CustomNpcsPermissions gate applies here.
button.enabled = RecipeController.Instance != null && !RecipeController.Instance.getAnvilList().isEmpty();
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
mc.renderEngine.bindTexture(resource);
@@ -36,6 +36,8 @@ public class GuiNpcCarpentryBench extends GuiContainerNPCInterface {
@Override
protected void drawGuiContainerBackgroundLayer(float f, int i, int j) {
// TODO: Reads the global RecipeController carpentry list before enabling; this bench is a public crafting block with no
// additional permission checks.
button.enabled = RecipeController.Instance != null && !RecipeController.Instance.carpentryRecipes.isEmpty();
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
mc.renderEngine.bindTexture(resource);
@@ -39,6 +39,8 @@ public class GuiRecipes extends GuiNPCInterface {
this.xSize = 256;
setBackground("recipes.png");
this.closeOnEsc = true;
// TODO: Preloads every carpentry recipe from RecipeController (full dataset); this book is open to any player, so no
// explicit permission is checked when accessing it.
recipes.addAll(RecipeController.Instance.carpentryRecipes.values());
}
@@ -74,9 +76,13 @@ public class GuiRecipes extends GuiNPCInterface {
private List<IRecipe> getSearchList() {
if (search.isEmpty()) {
// TODO: Returns the entire carpentry recipe collection from RecipeController; again this is unrestricted player
// content with no CustomNpcsPermissions gating.
return new ArrayList<IRecipe>(RecipeController.Instance.carpentryRecipes.values());
}
List<IRecipe> list = new ArrayList<IRecipe>();
// TODO: Iterates over the RecipeController carpentry map to filter recipes; this viewing GUI is available to all
// players without special permissions.
for (IRecipe recipe : RecipeController.Instance.carpentryRecipes.values()) {
if (recipe.getRecipeOutput() == null)
continue;
@@ -94,6 +94,8 @@ public class GuiCNPCInventory extends GuiNPCInterface {
PlayerData data = PlayerData.get(mc.thePlayer);
if (data != null && data.effectData != null) {
for (PlayerEffect pe : data.effectData.getEffects().values()) {
// TODO: Pulls specific CustomEffect entries from the global controller; this HUD is part of the base
// player inventory so it must work without extra permissions (no CustomNpcsPermissions required).
CustomEffect effect = CustomEffectController.getInstance().get(pe.id, pe.index);
if (effect != null) {
effectBar.entries.add(new GuiEffectBar.EffectEntry(effect, pe));
@@ -255,6 +255,8 @@ public class GuiModernQuestDialog extends GuiNPCInterface implements IGuiClose {
}
int fac1ID = quest.factionOptions.factionId;
if (fac1ID != -1) {
// TODO: Looks up faction display data from the global FactionController; this dialog is part of the standard
// player quest flow so no special permissions are needed beyond quest access.
String fac1Name = FactionController.getInstance().getFaction(fac1ID).getName();
String fac1Color = (quest.factionOptions.decreaseFactionPoints) ? "§c-" : "§a+";
int fac1Point = quest.factionOptions.factionPoints;
@@ -263,6 +265,8 @@ public class GuiModernQuestDialog extends GuiNPCInterface implements IGuiClose {
}
int fac2ID = quest.factionOptions.faction2Id;
if (fac2ID != -1) {
// TODO: Retrieves the second faction's metadata from FactionController as above; normal quest participants reach
// this without CustomNpcsPermissions requirements.
String fac2Name = FactionController.getInstance().getFaction(fac2ID).getName();
String fac2Color = (quest.factionOptions.decreaseFaction2Points) ? "§c-" : "§a+";
int fac2Point = quest.factionOptions.faction2Points;
@@ -79,7 +79,9 @@ public class GuiNpcQuestTypeItem extends GuiContainerNPCInterface implements ITe
// map.put(i, item.copy());
// }
// ((QuestItem)quest.questInterface).items = map;
// QuestController.saveQuest(quest);
// TODO: Saving would route through the global QuestController, which should stay limited to wand users with
// CustomNpcsPermissions.GLOBAL_QUEST or operators when OpsOnly is active.
// QuestController.saveQuest(quest);
}
@Override
@@ -307,6 +307,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb
}
if (guibutton.id == 106) {
// TODO: Opens the ScriptController shared directory; this screen is available only to editors with the scripter
// tool and CustomNpcsPermissions.TOOL_SCRIPTER.
NoppesUtil.openFolder(ScriptController.Instance.dir);
}
@@ -37,6 +37,8 @@ public class GuiDialogSelection extends SubGuiInterface implements ICustomScroll
setBackground("menubg.png");
xSize = 366;
ySize = 226;
// TODO: Pulls the entire DialogController dialog map (global data); players only reach this via wand-required dialog
// editors that enforce PacketUtil.verifyItemPacket and CustomNpcsPermissions.GLOBAL_DIALOG/ops when editing.
this.selectedDialog = DialogController.Instance.dialogs.get(dialog);
if (selectedDialog != null) {
selectedCategory = selectedDialog.category;
@@ -59,6 +61,8 @@ public class GuiDialogSelection extends SubGuiInterface implements ICustomScroll
HashMap<String, DialogCategory> categoryData = new HashMap<String, DialogCategory>();
HashMap<String, Dialog> dialogData = new HashMap<String, Dialog>();
// TODO: Iterates over every dialog category provided by DialogController; reachable through NPC wand workflows that
// require dialog-edit permissions (CustomNpcsPermissions.GLOBAL_DIALOG or op status under OpsOnly).
for (DialogCategory category : DialogController.Instance.categories.values()) {
categoryData.put(category.title, category);
}
@@ -37,6 +37,8 @@ public class GuiQuestSelection extends SubGuiInterface implements ICustomScrollL
setBackground("menubg.png");
xSize = 366;
ySize = 226;
// TODO: Requires the full QuestController quest map (global dataset); access comes from wand-driven quest editors
// that rely on PacketUtil.verifyItemPacket and ultimately CustomNpcsPermissions.GLOBAL_QUEST / server op checks.
this.selectedQuest = QuestController.Instance.quests.get(quest);
if (selectedQuest != null) {
selectedCategory = selectedQuest.category;
@@ -59,6 +61,8 @@ public class GuiQuestSelection extends SubGuiInterface implements ICustomScrollL
HashMap<String, QuestCategory> categoryData = new HashMap<String, QuestCategory>();
HashMap<String, Quest> questData = new HashMap<String, Quest>();
// TODO: Needs every quest category from QuestController so the selector can populate; opened from NPC wand flows that
// demand the editing player satisfy CustomNpcsPermissions.GLOBAL_QUEST (or be OP when OpsOnly is enabled).
for (QuestCategory category : QuestController.Instance.categories.values()) {
categoryData.put(category.title, category);
}