package openmods.sync; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.network.PacketBuffer; import openmods.Log; import openmods.utils.bitstream.OutputBitStream; public abstract class SyncMapServer extends SyncMap { private final Map<String, ISyncableObject> objects = Maps.newHashMap(); private static class Entry { private final String name; private final ISyncableObject obj; private final SyncableObjectType type; public Entry(String name, ISyncableObject obj) { this.name = name; this.obj = obj; this.type = SyncableObjectTypeRegistry.getType(obj.getClass()); Preconditions.checkNotNull(type, "Type %s is not registered", obj.getClass()); } } private final List<Entry> orderedEntries = Lists.newArrayList(); private final Map<ISyncableObject, Integer> objectToId = Maps.newIdentityHashMap(); private boolean firstRemoteObjectInitialized = false; private int bitmapLength; private final IUpdateStrategy updateStrategy; public SyncMapServer(UpdateStrategy strategy) { this.updateStrategy = strategy.create(this); } @Override public void registerObject(String name, ISyncableObject value) { Preconditions.checkState(!firstRemoteObjectInitialized, "Can't add fields to object that has already sent data to clients"); { final ISyncableObject prev = objects.put(name, value); Preconditions.checkState(prev == null, "Duplicate name '%s', %s -> %s", name, prev, value); } final int newId = orderedEntries.size(); orderedEntries.add(new Entry(name, value)); { final Integer prev = objectToId.put(value, newId); Preconditions.checkState(prev == null, "Duplicate object '%s', %s -> %s", name, prev, newId); } } @Override public void read(NBTTagCompound tag) { for (Map.Entry<String, ISyncableObject> entry : objects.entrySet()) { String name = entry.getKey(); final ISyncableObject obj = entry.getValue(); try { obj.readFromNBT(tag, name); } catch (Throwable e) { throw new SyncFieldException(e, name); } obj.markClean(); } } @Override public boolean tryRead(NBTTagCompound tag) { read(tag); return true; } @Override public void write(NBTTagCompound tag) { for (Map.Entry<String, ISyncableObject> entry : objects.entrySet()) { final String name = entry.getKey(); final ISyncableObject obj = entry.getValue(); try { obj.writeToNBT(tag, name); } catch (Throwable e) { throw new SyncFieldException(e, name); } } } @Override public boolean tryWrite(NBTTagCompound tag) { write(tag); return true; } @Override public void readIntializationData(PacketBuffer dis) { throw new UnsupportedOperationException(); } @Override public void readUpdate(PacketBuffer dis) { throw new UnsupportedOperationException(); } @Override public void writeInitializationData(PacketBuffer dos) throws IOException { updateStrategy.writeInitializationData(dos); } private void writeOwnerInfo(PacketBuffer dos) { dos.writeVarInt(getOwnerType()); writeOwnerData(dos); } private void writeSyncObjectInitialization(PacketBuffer dos) throws IOException { if (!firstRemoteObjectInitialized) { firstRemoteObjectInitialized = true; bitmapLength = (objects.size() + 7) / 8; } dos.writeVarInt(objects.size()); for (Entry e : orderedEntries) { dos.writeString(e.name); final int typeId = SyncableObjectTypeRegistry.getTypeId(e.type); dos.writeVarInt(typeId); e.obj.writeToStream(dos); } } private void writeUpdatePacket(PacketBuffer dos, Set<ISyncableObject> changes) throws IOException { Preconditions.checkState(firstRemoteObjectInitialized, "Remote objects not intialized yet"); final ByteBuf bitmapData = dos.slice(dos.writerIndex(), bitmapLength); bitmapData.clear(); dos.writeZero(bitmapLength); final OutputBitStream bitmap = new OutputBitStream(bitmapData::writeByte); for (Entry e : orderedEntries) { if (changes.contains(e.obj)) { e.obj.writeToStream(dos); bitmap.writeBit(true); } else { bitmap.writeBit(false); } } bitmap.flush(); } protected interface IUpdateStrategy { public void sendUpdates(Set<ISyncableObject> changedObjects); public void writeInitializationData(PacketBuffer dos) throws IOException; public boolean canSendUpdates(); } private class SeparateInitializationPacketStrategy implements IUpdateStrategy { @Override public void sendUpdates(Set<ISyncableObject> changedObjects) { if (changedObjects.isEmpty()) return; final Set<EntityPlayerMP> players = getPlayersWatching(); try { final PacketBuffer deltaPayload = new PacketBuffer(Unpooled.buffer()); writeOwnerInfo(deltaPayload); writeUpdatePacket(deltaPayload, changedObjects); SyncChannelHolder.INSTANCE.sendPayloadToPlayers(deltaPayload, players); } catch (IOException e) { Log.warn(e, "IOError during delta sync"); } } @Override public void writeInitializationData(PacketBuffer dos) throws IOException { // owner info not required, as initialization packet is assumed to already be directed writeSyncObjectInitialization(dos); } @Override public boolean canSendUpdates() { return firstRemoteObjectInitialized; } } private class SelfInitializingUpdateStrategy implements IUpdateStrategy { private Set<Integer> knownUsers = Sets.newHashSet(); @Override public void sendUpdates(Set<ISyncableObject> changes) { final boolean hasChanges = !changes.isEmpty(); List<EntityPlayerMP> fullPacketTargets = Lists.newArrayList(); List<EntityPlayerMP> deltaPacketTargets = Lists.newArrayList(); Set<EntityPlayerMP> players = getPlayersWatching(); for (EntityPlayerMP player : players) { if (knownUsers.contains(player.getEntityId())) { if (hasChanges) deltaPacketTargets.add(player); } else { knownUsers.add(player.getEntityId()); fullPacketTargets.add(player); } } try { if (!deltaPacketTargets.isEmpty()) { final PacketBuffer deltaPayload = new PacketBuffer(Unpooled.buffer()); writeOwnerInfo(deltaPayload); writeUpdatePacket(deltaPayload, changes); SyncChannelHolder.INSTANCE.sendPayloadToPlayers(deltaPayload, deltaPacketTargets); } } catch (IOException e) { Log.warn(e, "IOError during delta sync"); } try { if (!fullPacketTargets.isEmpty()) { final PacketBuffer fullPayload = new PacketBuffer(Unpooled.buffer()); writeOwnerInfo(fullPayload); writeSyncObjectInitialization(fullPayload); SyncChannelHolder.INSTANCE.sendPayloadToPlayers(fullPayload, fullPacketTargets); } } catch (IOException e) { Log.warn(e, "IOError during full sync"); } } @Override public void writeInitializationData(PacketBuffer dos) { // use other strategy, if you want to send update packet throw new UnsupportedOperationException(); } @Override public boolean canSendUpdates() { return true; } } public enum UpdateStrategy { WITHOUT_INITIAL_PACKET { @Override protected IUpdateStrategy create(SyncMapServer owner) { return owner.new SelfInitializingUpdateStrategy(); } }, WITH_INITIAL_PACKET { @Override protected IUpdateStrategy create(SyncMapServer owner) { return owner.new SeparateInitializationPacketStrategy(); } }; protected abstract IUpdateStrategy create(SyncMapServer owner); } private Set<ISyncableObject> listChanges() { Set<ISyncableObject> changes = Sets.newIdentityHashSet(); for (Entry e : orderedEntries) if (e.obj.isDirty()) { changes.add(e.obj); e.obj.markClean(); } return changes; } protected final Set<ISyncListener> syncListeners = Sets.newIdentityHashSet(); @Override public void addSyncListener(ISyncListener listener) { syncListeners.add(listener); } @Override public void addUpdateListener(ISyncListener listener) { // NO-OP } @Override public void removeUpdateListener(ISyncListener dispatcher) { // NO-OP } @Override public void sendUpdates() { if (isInvalid() || !updateStrategy.canSendUpdates()) return; final Set<ISyncableObject> changedObjects = listChanges(); updateStrategy.sendUpdates(changedObjects); if (!changedObjects.isEmpty()) { notifySyncListeners(syncListeners, Collections.unmodifiableSet(changedObjects)); } } @Override public boolean trySendUpdates() { sendUpdates(); return true; } @Override public ISyncableObject getObjectById(int objectId) { try { return orderedEntries.get(objectId).obj; } catch (IndexOutOfBoundsException e) { throw new NoSuchElementException(Integer.toString(objectId)); } } @Override public int getObjectId(ISyncableObject object) { Integer result = objectToId.get(object); if (result == null) throw new NoSuchElementException(String.valueOf(object)); return result; } protected abstract int getOwnerType(); protected abstract void writeOwnerData(PacketBuffer output); protected abstract Set<EntityPlayerMP> getPlayersWatching(); protected abstract boolean isInvalid(); }