package com.minemaarten.signals.rail.network.mc; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.regex.Pattern; import java.util.stream.Stream; import net.minecraft.entity.Entity; import net.minecraft.entity.item.EntityMinecart; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.EnumFacing; import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.ITextComponent; import net.minecraft.util.text.TextComponentTranslation; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraftforge.common.DimensionManager; import net.minecraftforge.fml.common.FMLCommonHandler; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.minemaarten.signals.Signals; import com.minemaarten.signals.api.access.ISignal.EnumLampStatus; import com.minemaarten.signals.config.SignalsConfig; import com.minemaarten.signals.network.NetworkHandler; import com.minemaarten.signals.network.PacketAddOrUpdateTrain; import com.minemaarten.signals.network.PacketClearNetwork; import com.minemaarten.signals.network.PacketUpdateNetwork; import com.minemaarten.signals.rail.network.EnumHeading; import com.minemaarten.signals.rail.network.INetworkObject; import com.minemaarten.signals.rail.network.NetworkRail; import com.minemaarten.signals.rail.network.NetworkUpdater; import com.minemaarten.signals.rail.network.RailNetwork; import com.minemaarten.signals.rail.network.RailNetworkClient; import com.minemaarten.signals.rail.network.RailPathfinder; import com.minemaarten.signals.rail.network.RailRoute.RailRouteResult; import com.minemaarten.signals.rail.network.Train; import com.minemaarten.signals.tileentity.TileEntityBase; public class RailNetworkManager{ private static RailNetworkManager CLIENT_INSTANCE; private static RailNetworkManager SERVER_INSTANCE; public static RailNetworkManager getInstance(boolean clientInstance){ return clientInstance ? getClientInstance() : getServerInstance(); } public static RailNetworkManager getClientInstance(){ if(CLIENT_INSTANCE == null) { CLIENT_INSTANCE = new RailNetworkManager(true); } return CLIENT_INSTANCE; } public static RailNetworkManager getServerInstance(){ if(SERVER_INSTANCE == null) { SERVER_INSTANCE = new RailNetworkManager(false); } return SERVER_INSTANCE; } private final ExecutorService railNetworkExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("signals-network-thread-%d").build()); private Future<RailNetwork<MCPos>> networkUpdateTask; private RailNetwork<MCPos> network; private MCNetworkState state = new MCNetworkState(this); private final NetworkUpdater<MCPos> networkUpdater = new NetworkUpdater<>(new NetworkObjectProvider()); private RailNetworkManager(boolean client){ if(client) { network = RailNetworkClient.empty(); } else { network = RailNetwork.empty(); } } private void validateOnServer(){ if(this == CLIENT_INSTANCE) throw new IllegalStateException(); } private void validateOnClient(){ if(this == SERVER_INSTANCE) throw new IllegalStateException(); } public boolean isClientInstance(){ return this == CLIENT_INSTANCE; } /** * The initial nodes used to build out the network from. * Signals, Station Markers, rail links. Only used when force rebuilding the network. * @return */ private Set<MCPos> getStartNodes(){ Set<MCPos> nodes = new HashSet<>(); for(World world : DimensionManager.getWorlds()) { for(TileEntity te : world.loadedTileEntityList) { if(te instanceof TileEntityBase) { //Any Signals TE for testing purposes nodes.add(new MCPos(world, te.getPos())); for(EnumFacing facing : EnumFacing.VALUES) { BlockPos pos = te.getPos().offset(facing); nodes.add(new MCPos(world, pos)); } } } } return nodes; } public void rebuildNetwork(){ validateOnServer(); NetworkHandler.sendToAll(new PacketClearNetwork()); network = RailNetwork.empty(); getStartNodes().forEach(networkUpdater::markDirty); initTrains(); state.update(network); } private void initTrains(){ List<EntityMinecart> carts = new ArrayList<>(); for(World world : DimensionManager.getWorlds()) { for(Entity entity : world.loadedEntityList) { if(entity instanceof EntityMinecart) { carts.add((EntityMinecart)entity); } } } Set<MCTrain> trains = new NetworkObjectProvider().provideTrains(carts); state.setTrains(trains); for(MCTrain train : trains) { NetworkHandler.sendToAll(new PacketAddOrUpdateTrain(train)); } } public RailNetwork<MCPos> getNetwork(){ return network; } public RailNetworkClient<MCPos> getClientNetwork(){ return (RailNetworkClient<MCPos>)network; } public MCNetworkState getState(){ return state; } public NetworkRail<MCPos> getRail(World world, BlockPos pos){ return getRail(new MCPos(world, pos)); } public NetworkRail<MCPos> getRail(MCPos pos){ return network.railObjects.getRail(pos); } public void loadNetwork(RailNetwork<MCPos> network, MCNetworkState state){ networkUpdateTask = null; state.getTrackingCartsFrom(this.state); // Take carts that were loaded before this network state was loaded from nbt. this.network = network; this.state = state; NetworkHandler.sendToAll(new PacketClearNetwork()); for(PacketUpdateNetwork packet : getSplitNetworkUpdatePackets(network.railObjects.getAllNetworkObjects().values())) { NetworkHandler.sendToAll(packet); } for(Train<MCPos> train : state.getTrains()) { NetworkHandler.sendToAll(new PacketAddOrUpdateTrain((MCTrain)train)); } } public MCTrain getTrainByID(int id){ return (MCTrain)state.getTrain(id); } public Stream<MCTrain> getAllTrains(){ return state.getTrainStream().map(t -> (MCTrain)t); } public void addTrain(MCTrain train){ if(this == SERVER_INSTANCE) { NetworkHandler.sendToAll(new PacketAddOrUpdateTrain(train)); } state.addTrain(train); } public void removeTrain(int trainID){ state.removeTrain(trainID); } public EnumLampStatus getLampStatus(World world, BlockPos pos){ return state.getLampStatus(new MCPos(world, pos)); } public RailRouteResult<MCPos> pathfind(MCPos start, Train<MCPos> train, Pattern destinationRegex, EnumHeading direction){ return new RailPathfinder<MCPos>(network, state).pathfindToDestination(start, train, destinationRegex, direction); } public void markDirty(MCPos pos){ validateOnServer(); networkUpdater.markDirty(pos); } public void onPreServerTick(){ if(!SignalsConfig.enableRailNetwork) return; Collection<INetworkObject<MCPos>> updates = networkUpdater.getNetworkUpdates(network); if(!updates.isEmpty()) { applyUpdates(updates); for(PacketUpdateNetwork packet : getSplitNetworkUpdatePackets(updates)) { NetworkHandler.sendToAll(packet); } } } public void checkForNewNetwork(boolean forceWait){ if(networkUpdateTask != null && (forceWait || networkUpdateTask.isDone())) { try { network = networkUpdateTask.get(); networkUpdateTask = null; NetworkStorage.getInstance(isClientInstance()).setNetwork(network); if(this == CLIENT_INSTANCE) { //Asynchronously update the renderers railNetworkExecutor.submit(() -> { network.build(); //Build the network cache off thread Signals.proxy.onRailNetworkUpdated(); }); } state.onNetworkChanged(network); } catch(InterruptedException e) { e.printStackTrace(); } catch(ExecutionException e) { throw new RuntimeException(e); } } } /** * Asynchronously calculates the new rail network. * This is possible because no MC interaction, and immutable objects. * @param changedObjects */ public void applyUpdates(Collection<INetworkObject<MCPos>> changedObjects){ if(this == SERVER_INSTANCE || networkUpdateTask == null) { checkForNewNetwork(true); networkUpdateTask = railNetworkExecutor.submit(() -> networkUpdater.applyUpdates(getNetwork(), changedObjects).build()); } else { //On the client, when the network was already updating, simply schedule the new update after the current one. final Future<RailNetwork<MCPos>> prevTask = networkUpdateTask; networkUpdateTask = railNetworkExecutor.submit(() -> { return networkUpdater.applyUpdates(prevTask.get(), changedObjects);//Update from the previous update, and wait for this. }); } } public void clearNetwork(){ validateOnClient(); network = RailNetworkClient.empty(); state.setTrains(Collections.emptyList()); Signals.proxy.onRailNetworkUpdated(); } public void onPostServerTick(){ if(!SignalsConfig.enableRailNetwork) return; validateOnServer(); checkForNewNetwork(true); state.update(network); if(networkUpdater.didJustTurnBusy()) { notifyAllPlayers(new TextComponentTranslation("signals.message.signals_busy")); } if(networkUpdater.didJustTurnIdle()) { notifyAllPlayers(new TextComponentTranslation("signals.message.signals_idle")); } } private void notifyAllPlayers(ITextComponent text){ for(EntityPlayer player : FMLCommonHandler.instance().getMinecraftServerInstance().getPlayerList().getPlayers()) { player.sendMessage(text); } } public void onPreClientTick(){ if(!SignalsConfig.enableRailNetwork) return; validateOnClient(); checkForNewNetwork(false); //Log.info("Trains: " + getAllTrains().count()); } public void onPlayerJoin(EntityPlayerMP player){ NetworkHandler.sendTo(new PacketClearNetwork(), player); for(PacketUpdateNetwork packet : getSplitNetworkUpdatePackets(network.railObjects.getAllNetworkObjects().values())) { NetworkHandler.sendTo(packet, player); } state.onPlayerJoin(player); } public void onChunkUnload(Chunk chunk){ state.onChunkUnload(chunk); } public void onMinecartJoinedWorld(EntityMinecart cart){ state.onMinecartJoinedWorld(cart); } public void onCartRemoved(EntityMinecart cart){ state.removeCart(cart); } private static final int MAX_CHANGES_PER_PACKET = 1000; private List<PacketUpdateNetwork> getSplitNetworkUpdatePackets(Collection<INetworkObject<MCPos>> allChangedObjects){ if(allChangedObjects.size() <= MAX_CHANGES_PER_PACKET) return Collections.singletonList(new PacketUpdateNetwork(allChangedObjects)); List<PacketUpdateNetwork> packets = new ArrayList<>(); Iterator<INetworkObject<MCPos>> iterator = allChangedObjects.iterator(); List<INetworkObject<MCPos>> changedObjects = new ArrayList<>(MAX_CHANGES_PER_PACKET); while(iterator.hasNext()) { changedObjects.add(iterator.next()); if(changedObjects.size() >= MAX_CHANGES_PER_PACKET) { packets.add(new PacketUpdateNetwork(changedObjects)); changedObjects = new ArrayList<>(MAX_CHANGES_PER_PACKET); } } if(!changedObjects.isEmpty()) packets.add(new PacketUpdateNetwork(changedObjects)); return packets; } }