package erogenousbeef.core.multiblock; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import net.minecraft.world.ChunkCoordIntPair; import net.minecraft.world.World; import net.minecraft.world.chunk.IChunkProvider; import erogenousbeef.core.common.BeefCoreLog; import erogenousbeef.core.common.CoordTriplet; /** * This class manages all the multiblock controllers that exist in a given world, * either client- or server-side. * You must create different registries for server and client worlds. * * @author Erogenous Beef */ public class MultiblockWorldRegistry { private World worldObj; private Set<MultiblockControllerBase> controllers; // Active controllers private Set<MultiblockControllerBase> dirtyControllers; // Controllers whose parts lists have changed private Set<MultiblockControllerBase> deadControllers; // Controllers which are empty // A list of orphan parts - parts which currently have no master, but should seek one this tick // Indexed by the hashed chunk coordinate // This can be added-to asynchronously via chunk loads! private Set<IMultiblockPart> orphanedParts; // A list of parts which have been detached during internal operations private Set<IMultiblockPart> detachedParts; // A list of parts whose chunks have not yet finished loading // They will be added to the orphan list when they are finished loading. // Indexed by the hashed chunk coordinate // This can be added-to asynchronously via chunk loads! private HashMap<Long, Set<IMultiblockPart>> partsAwaitingChunkLoad; // Mutexes to protect lists which may be changed due to asynchronous events, such as chunk loads private Object partsAwaitingChunkLoadMutex; private Object orphanedPartsMutex; public MultiblockWorldRegistry(World world) { worldObj = world; controllers = new HashSet<MultiblockControllerBase>(); deadControllers = new HashSet<MultiblockControllerBase>(); dirtyControllers = new HashSet<MultiblockControllerBase>(); detachedParts = new HashSet<IMultiblockPart>(); orphanedParts = new HashSet<IMultiblockPart>(); partsAwaitingChunkLoad = new HashMap<Long, Set<IMultiblockPart>>(); partsAwaitingChunkLoadMutex = new Object(); orphanedPartsMutex = new Object(); } /** * Called before Tile Entities are ticked in the world. Run game logic. */ public void tickStart() { if(controllers.size() > 0) { for(MultiblockControllerBase controller : controllers) { if(controller.worldObj == worldObj && controller.worldObj.isRemote == worldObj.isRemote) { if(controller.isEmpty()) { // This happens on the server when the user breaks the last block. It's fine. // Mark 'er dead and move on. deadControllers.add(controller); } else { // Run the game logic for this world controller.updateMultiblockEntity(); } } } } } /** * Called prior to processing multiblock controllers. Do bookkeeping. */ public void processMultiblockChanges() { IChunkProvider chunkProvider = worldObj.getChunkProvider(); CoordTriplet coord; // Merge pools - sets of adjacent machines which should be merged later on in processing List<Set<MultiblockControllerBase>> mergePools = null; if(orphanedParts.size() > 0) { Set<IMultiblockPart> orphansToProcess = null; // Keep the synchronized block small. We can't iterate over orphanedParts directly // because the client does not know which chunks are actually loaded, so attachToNeighbors() // is not chunk-safe on the client, because Minecraft is stupid. // It's possible to polyfill this, but the polyfill is too slow for comfort. synchronized(orphanedPartsMutex) { if(orphanedParts.size() > 0) { orphansToProcess = orphanedParts; orphanedParts = new HashSet<IMultiblockPart>(); } } if(orphansToProcess != null && orphansToProcess.size() > 0) { Set<MultiblockControllerBase> compatibleControllers; // Process orphaned blocks // These are blocks that exist in a valid chunk and require a controller for(IMultiblockPart orphan : orphansToProcess) { coord = orphan.getWorldLocation(); if(!chunkProvider.chunkExists(coord.getChunkX(), coord.getChunkZ())) { continue; } // This can occur on slow machines. if(orphan.isInvalid()) { continue; } if(worldObj.getTileEntity(coord.x, coord.y, coord.z) != orphan) { // This block has been replaced by another. continue; } // THIS IS THE ONLY PLACE WHERE PARTS ATTACH TO MACHINES // Try to attach to a neighbor's master controller compatibleControllers = orphan.attachToNeighbors(); if(compatibleControllers == null) { // FOREVER ALONE! Create and register a new controller. // THIS IS THE ONLY PLACE WHERE NEW CONTROLLERS ARE CREATED. MultiblockControllerBase newController = orphan.createNewMultiblock(); newController.attachBlock(orphan); this.controllers.add(newController); } else if(compatibleControllers.size() > 1) { if(mergePools == null) { mergePools = new ArrayList<Set<MultiblockControllerBase>>(); } // THIS IS THE ONLY PLACE WHERE MERGES ARE DETECTED // Multiple compatible controllers indicates an impending merge. // Locate the appropriate merge pool(s) boolean hasAddedToPool = false; List<Set<MultiblockControllerBase>> candidatePools = new ArrayList<Set<MultiblockControllerBase>>(); for(Set<MultiblockControllerBase> candidatePool : mergePools) { if(!Collections.disjoint(candidatePool, compatibleControllers)) { // They share at least one element, so that means they will all touch after the merge candidatePools.add(candidatePool); } } if(candidatePools.size() <= 0) { // No pools nearby, create a new merge pool mergePools.add(compatibleControllers); } else if(candidatePools.size() == 1) { // Only one pool nearby, simply add to that one candidatePools.get(0).addAll(compatibleControllers); } else { // Multiple pools- merge into one, then add the compatible controllers Set<MultiblockControllerBase> masterPool = candidatePools.get(0); Set<MultiblockControllerBase> consumedPool; for(int i = 1; i < candidatePools.size(); i++) { consumedPool = candidatePools.get(i); masterPool.addAll(consumedPool); mergePools.remove(consumedPool); } masterPool.addAll(compatibleControllers); } } } } } if(mergePools != null && mergePools.size() > 0) { // Process merges - any machines that have been marked for merge should be merged // into the "master" machine. // To do this, we combine lists of machines that are touching one another and therefore // should voltron the fuck up. for(Set<MultiblockControllerBase> mergePool : mergePools) { // Search for the new master machine, which will take over all the blocks contained in the other machines MultiblockControllerBase newMaster = null; for(MultiblockControllerBase controller : mergePool) { if(newMaster == null || controller.shouldConsume(newMaster)) { newMaster = controller; } } if(newMaster == null) { BeefCoreLog.fatal("Multiblock system checked a merge pool of size %d, found no master candidates. This should never happen.", mergePool.size()); } else { // Merge all the other machines into the master machine, then unregister them addDirtyController(newMaster); for(MultiblockControllerBase controller : mergePool) { if(controller != newMaster) { newMaster.assimilate(controller); addDeadController(controller); addDirtyController(newMaster); } } } } } // Process splits and assembly // Any controllers which have had parts removed must be checked to see if some parts are no longer // physically connected to their master. if(dirtyControllers.size() > 0) { Set<IMultiblockPart> newlyDetachedParts = null; for(MultiblockControllerBase controller : dirtyControllers) { // Tell the machine to check if any parts are disconnected. // It should return a set of parts which are no longer connected. // POSTCONDITION: The controller must have informed those parts that // they are no longer connected to this machine. newlyDetachedParts = controller.checkForDisconnections(); if(!controller.isEmpty()) { controller.recalculateMinMaxCoords(); controller.checkIfMachineIsWhole(); } else { addDeadController(controller); } if(newlyDetachedParts != null && newlyDetachedParts.size() > 0) { // Controller has shed some parts - add them to the detached list for delayed processing detachedParts.addAll(newlyDetachedParts); } } dirtyControllers.clear(); } // Unregister dead controllers if(deadControllers.size() > 0) { for(MultiblockControllerBase controller : deadControllers) { // Go through any controllers which have marked themselves as potentially dead. // Validate that they are empty/dead, then unregister them. if(!controller.isEmpty()) { BeefCoreLog.fatal("Found a non-empty controller. Forcing it to shed its blocks and die. This should never happen!"); detachedParts.addAll(controller.detachAllBlocks()); } // THIS IS THE ONLY PLACE WHERE CONTROLLERS ARE UNREGISTERED. this.controllers.remove(controller); } deadControllers.clear(); } // Process detached blocks // Any blocks which have been detached this tick should be moved to the orphaned // list, and will be checked next tick to see if their chunk is still loaded. for(IMultiblockPart part : detachedParts) { // Ensure parts know they're detached part.assertDetached(); } addAllOrphanedPartsThreadsafe(detachedParts); detachedParts.clear(); } /** * Called when a multiblock part is added to the world, either via chunk-load or user action. * If its chunk is loaded, it will be processed during the next tick. * If the chunk is not loaded, it will be added to a list of objects waiting for a chunkload. * @param part The part which is being added to this world. */ public void onPartAdded(IMultiblockPart part) { CoordTriplet worldLocation = part.getWorldLocation(); if(!worldObj.getChunkProvider().chunkExists(worldLocation.getChunkX(), worldLocation.getChunkZ())) { // Part goes into the waiting-for-chunk-load list Set<IMultiblockPart> partSet; long chunkHash = worldLocation.getChunkXZHash(); synchronized(partsAwaitingChunkLoadMutex) { if(!partsAwaitingChunkLoad.containsKey(chunkHash)) { partSet = new HashSet<IMultiblockPart>(); partsAwaitingChunkLoad.put(chunkHash, partSet); } else { partSet = partsAwaitingChunkLoad.get(chunkHash); } partSet.add(part); } } else { // Part goes into the orphan queue, to be checked this tick addOrphanedPartThreadsafe(part); } } /** * Called when a part is removed from the world, via user action or via chunk unloads. * This part is removed from any lists in which it may be, and its machine is marked for recalculation. * @param part The part which is being removed. */ public void onPartRemovedFromWorld(IMultiblockPart part) { CoordTriplet coord = part.getWorldLocation(); if(coord != null) { long hash = coord.getChunkXZHash(); if(partsAwaitingChunkLoad.containsKey(hash)) { synchronized(partsAwaitingChunkLoadMutex) { if(partsAwaitingChunkLoad.containsKey(hash)) { partsAwaitingChunkLoad.get(hash).remove(part); if(partsAwaitingChunkLoad.get(hash).size() <= 0) { partsAwaitingChunkLoad.remove(hash); } } } } } detachedParts.remove(part); if(orphanedParts.contains(part)) { synchronized(orphanedPartsMutex) { orphanedParts.remove(part); } } part.assertDetached(); } /** * Called when the world which this World Registry represents is fully unloaded from the system. * Does some housekeeping just to be nice. */ public void onWorldUnloaded() { controllers.clear(); deadControllers.clear(); dirtyControllers.clear(); detachedParts.clear(); synchronized(partsAwaitingChunkLoadMutex) { partsAwaitingChunkLoad.clear(); } synchronized(orphanedPartsMutex) { orphanedParts.clear(); } worldObj = null; } /** * Called when a chunk has finished loading. Adds all of the parts which are awaiting * load to the list of parts which are orphans and therefore will be added to machines * after the next world tick. * * @param chunkX Chunk X coordinate (world coordate >> 4) of the chunk that was loaded * @param chunkZ Chunk Z coordinate (world coordate >> 4) of the chunk that was loaded */ public void onChunkLoaded(int chunkX, int chunkZ) { long chunkHash = ChunkCoordIntPair.chunkXZ2Int(chunkX, chunkZ); if(partsAwaitingChunkLoad.containsKey(chunkHash)) { synchronized(partsAwaitingChunkLoadMutex) { if(partsAwaitingChunkLoad.containsKey(chunkHash)) { addAllOrphanedPartsThreadsafe(partsAwaitingChunkLoad.get(chunkHash)); partsAwaitingChunkLoad.remove(chunkHash); } } } } /** * Registers a controller as dead. It will be cleaned up at the end of the next world tick. * Note that a controller must shed all of its blocks before being marked as dead, or the system * will complain at you. * * @param deadController The controller which is dead. */ public void addDeadController(MultiblockControllerBase deadController) { this.deadControllers.add(deadController); } /** * Registers a controller as dirty - its list of attached blocks has changed, and it * must be re-checked for assembly and, possibly, for orphans. * * @param dirtyController The dirty controller. */ public void addDirtyController(MultiblockControllerBase dirtyController) { this.dirtyControllers.add(dirtyController); } /** * Use this only if you know what you're doing. You should rarely need to iterate * over all controllers in a world! * * @return An (unmodifiable) set of controllers which are active in this world. */ public Set<MultiblockControllerBase> getControllers() { return Collections.unmodifiableSet(controllers); } /* *** PRIVATE HELPERS *** */ private void addOrphanedPartThreadsafe(IMultiblockPart part) { synchronized(orphanedPartsMutex) { orphanedParts.add(part); } } private void addAllOrphanedPartsThreadsafe(Collection<? extends IMultiblockPart> parts) { synchronized(orphanedPartsMutex) { orphanedParts.addAll(parts); } } private String clientOrServer() { return worldObj.isRemote ? "CLIENT" : "SERVER"; } }