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 cpw.mods.fml.common.network.ByteBufUtils; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; import java.io.DataInput; import java.io.DataInputStream; import java.io.DataOutput; import java.io.DataOutputStream; import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; import net.minecraft.world.World; import openmods.Log; import openmods.utils.ByteUtils; public abstract class SyncMap<H extends ISyncMapProvider> { public static class SyncFieldException extends RuntimeException { private static final long serialVersionUID = -3154521464407191767L; public SyncFieldException(Throwable cause, String name) { super(String.format("Failed to sync field '%s'", name), cause); } public SyncFieldException(Throwable cause, int index) { super(String.format("Failed to sync field #%d", index), cause); } } private static final int MAX_OBJECT_NUM = Short.SIZE; public enum HandlerType { TILE_ENTITY { @Override public ISyncMapProvider findHandler(World world, DataInput input) throws IOException { int x = input.readInt(); int y = input.readInt(); int z = input.readInt(); if (world != null) { if (world.blockExists(x, y, z)) { TileEntity tile = world.getTileEntity(x, y, z); if (tile instanceof ISyncMapProvider) return (ISyncMapProvider)tile; } } Log.warn("Invalid handler info: can't find ISyncHandler TE @ (%d,%d,%d)", x, y, z); return null; } @Override public void writeHandlerInfo(ISyncMapProvider handler, DataOutput output) throws IOException { try { TileEntity te = (TileEntity)handler; output.writeInt(te.xCoord); output.writeInt(te.yCoord); output.writeInt(te.zCoord); } catch (ClassCastException e) { throw new RuntimeException("Invalid usage of handler type", e); } } }, ENTITY { @Override public ISyncMapProvider findHandler(World world, DataInput input) throws IOException { int entityId = input.readInt(); Entity entity = world.getEntityByID(entityId); if (entity instanceof ISyncMapProvider) return (ISyncMapProvider)entity; Log.warn("Invalid handler info: can't find ISyncHandler entity id %d", entityId); return null; } @Override public void writeHandlerInfo(ISyncMapProvider handler, DataOutput output) throws IOException { try { Entity e = (Entity)handler; output.writeInt(e.getEntityId()); } catch (ClassCastException e) { throw new RuntimeException("Invalid usage of handler type", e); } } }; public abstract ISyncMapProvider findHandler(World world, DataInput input) throws IOException; public abstract void writeHandlerInfo(ISyncMapProvider handler, DataOutput output) throws IOException; private static final HandlerType[] TYPES = values(); } protected final H handler; private Set<Integer> knownUsers = Sets.newHashSet(); private ISyncableObject[] objects = new ISyncableObject[16]; private Map<String, ISyncableObject> nameMap = Maps.newHashMap(); private Map<ISyncableObject, Integer> objectToId = Maps.newIdentityHashMap(); private Set<ISyncListener> syncListeners = Sets.newIdentityHashSet(); private Set<ISyncListener> updateListeners = Sets.newIdentityHashSet(); private int index = 0; protected SyncMap(H handler) { this.handler = handler; } public void put(String name, ISyncableObject value) { Preconditions.checkState(index < MAX_OBJECT_NUM, "Can't add more than %s objects", MAX_OBJECT_NUM); int objId = index++; objects[objId] = value; nameMap.put(name, value); Integer prev = objectToId.put(value, objId); Preconditions.checkState(prev == null, "Object %s registered twice, under ids %s and %s", value, prev, objId); } public ISyncableObject get(String name) { ISyncableObject result = nameMap.get(name); if (result == null) throw new NoSuchElementException(name); return result; } public ISyncableObject get(int objectId) { try { return objects[objectId]; } catch (ArrayIndexOutOfBoundsException e) { throw new NoSuchElementException(Integer.toString(objectId)); } } public int getId(ISyncableObject object) { Integer result = objectToId.get(object); if (result == null) throw new NoSuchElementException(String.valueOf(object)); return result; } public int size() { return index; } public void readFromStream(DataInputStream dis) throws IOException { int mask = dis.readShort(); Set<ISyncableObject> changes = Sets.newIdentityHashSet(); int currentBit = 0; while (mask != 0) { if ((mask & 1) != 0) { final ISyncableObject object = objects[currentBit]; if (object != null) { try { object.readFromStream(dis); } catch (Throwable t) { throw new SyncFieldException(t, currentBit); } changes.add(object); } } currentBit++; mask >>= 1; } if (!changes.isEmpty()) notifySyncListeners(updateListeners, Collections.unmodifiableSet(changes)); } private void writeToStream(DataOutputStream dos, boolean fullPacket) throws IOException { int mask = 0; for (int i = 0; i < index; i++) { final ISyncableObject object = objects[i]; if (object != null && (fullPacket || object.isDirty())) { mask = ByteUtils.on(mask, i); } } dos.writeShort(mask); for (int i = 0; i < index; i++) { final ISyncableObject object = objects[i]; if (object != null && (fullPacket || object.isDirty())) { try { object.writeToStream(dos); } catch (Throwable t) { throw new SyncFieldException(t, i); } } } } protected abstract HandlerType getHandlerType(); protected abstract Set<EntityPlayerMP> getPlayersWatching(); protected abstract World getWorld(); protected abstract boolean isInvalid(); public void sync() { Preconditions.checkState(!getWorld().isRemote, "This method can only be used server side"); if (isInvalid()) return; Set<ISyncableObject> changes = listChanges(); 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()) { ByteBuf deltaPayload = createPayload(false); SyncChannelHolder.INSTANCE.sendPayloadToPlayers(deltaPayload, deltaPacketTargets); } } catch (IOException e) { Log.warn(e, "IOError during delta sync"); } try { if (!fullPacketTargets.isEmpty()) { ByteBuf deltaPayload = createPayload(true); SyncChannelHolder.INSTANCE.sendPayloadToPlayers(deltaPayload, fullPacketTargets); } } catch (IOException e) { Log.warn(e, "IOError during full sync"); } if (hasChanges) { unmarkChanges(changes); notifySyncListeners(syncListeners, Collections.unmodifiableSet(changes)); } } private Set<ISyncableObject> listChanges() { Set<ISyncableObject> changes = Sets.newIdentityHashSet(); for (int i = 0; i < index; i++) { ISyncableObject obj = objects[i]; if (obj != null && obj.isDirty()) changes.add(obj); } return changes; } private static void unmarkChanges(Set<ISyncableObject> changes) { for (ISyncableObject obj : changes) obj.markClean(); } public ByteBuf createPayload(boolean fullPacket) throws IOException { ByteBuf output = Unpooled.buffer(); HandlerType type = getHandlerType(); ByteBufUtils.writeVarInt(output, type.ordinal(), 5); DataOutputStream dataOutput = new DataOutputStream(new ByteBufOutputStream(output)); type.writeHandlerInfo(handler, dataOutput); writeToStream(dataOutput, fullPacket); return output.copy(); } public static ISyncMapProvider findSyncMap(World world, DataInput input) throws IOException { int handlerTypeId = ByteUtils.readVLI(input); // If this happens, abort! Serious bug! Preconditions.checkPositionIndex(handlerTypeId, HandlerType.TYPES.length, "handler type"); HandlerType handlerType = HandlerType.TYPES[handlerTypeId]; ISyncMapProvider handler = handlerType.findHandler(world, input); return handler; } public void writeToNBT(NBTTagCompound tag) { for (Entry<String, ISyncableObject> entry : nameMap.entrySet()) { final String name = entry.getKey(); final ISyncableObject obj = entry.getValue(); try { obj.writeToNBT(tag, name); } catch (Throwable e) { throw new SyncFieldException(e, name); } } } public void readFromNBT(NBTTagCompound tag) { for (Entry<String, ISyncableObject> entry : nameMap.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(); } } private static void notifySyncListeners(Collection<ISyncListener> listeners, Set<ISyncableObject> allChanges) { for (ISyncListener listener : listeners) listener.onSync(allChanges); } public void addSyncListener(ISyncListener listener) { syncListeners.add(listener); } public void addUpdateListener(ISyncListener listener) { updateListeners.add(listener); } public void removeUpdateListener(ISyncListener dispatcher) { updateListeners.remove(dispatcher); } }