/* * This file is part of Applied Energistics 2. * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Applied Energistics 2 is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Applied Energistics 2. If not, see . */ package appeng.hooks; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.TimeUnit; import com.google.common.base.Stopwatch; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.minecraft.server.MinecraftServer; import net.minecraft.server.world.ServerWorld; import net.minecraft.world.World; import net.minecraft.world.WorldAccess; import appeng.api.networking.IGridNode; import appeng.api.util.AEColor; import appeng.core.AEConfig; import appeng.core.AELog; import appeng.core.sync.packets.PaintedEntityPacket; import appeng.crafting.CraftingJob; import appeng.me.Grid; import appeng.tile.AEBaseBlockEntity; import appeng.util.IWorldCallable; import appeng.util.Platform; public class TickHandler { private static TickHandler INSTANCE; private final Queue> serverQueue = new ArrayDeque<>(); private final Multimap craftingJobs = LinkedListMultimap.create(); private final Map>> callQueue = new WeakHashMap<>(); private final HandlerRep server = new HandlerRep(); private final HandlerRep client = new HandlerRep(); private final HashMap srvPlayerColors = new HashMap<>(); public TickHandler() { if (INSTANCE != null) { throw new IllegalStateException("There can only be a single tick handler."); } INSTANCE = this; // Register for all the tick events we care about ServerTickEvents.END_SERVER_TICK.register(this::onAfterServerTick); ServerTickEvents.START_WORLD_TICK.register(this::onBeforeWorldTick); ServerTickEvents.END_WORLD_TICK.register(this::onAfterWorldTick); ServerLifecycleEvents.SERVER_STOPPING.register(this::onServerStopping); } public static TickHandler instance() { return INSTANCE; } public Map getPlayerColors() { return this.srvPlayerColors; } public void addCallable(final WorldAccess w, final IWorldCallable c) { if (w == null) { this.serverQueue.add(c); } else { Queue> queue = this.callQueue.get(w); if (queue == null) { queue = new ArrayDeque<>(); this.callQueue.put(w, queue); } queue.add(c); } } public void addInit(final AEBaseBlockEntity tile) { // for no there is no reason to care about this on the client... if (Platform.isServer()) { this.getRepo().tiles.add(tile); } } private HandlerRep getRepo() { if (Platform.isServer()) { return this.server; } return this.client; } public void addNetwork(final Grid grid) { // for no there is no reason to care about this on the client... if (Platform.isServer()) { this.getRepo().addNetwork(grid); } } public void removeNetwork(final Grid grid) { // for no there is no reason to care about this on the client... if (Platform.isServer()) { this.getRepo().removeNetwork(grid); } } public Iterable getGridList() { return this.getRepo().networks; } public void shutdown() { this.getRepo().clear(); } // FIXME FABRIC It does not look like worlds can ever unload in Fabric. // FIXME FABRIC public void unloadWorld(final WorldEvent.Unload ev) { // FIXME FABRIC if (Platform.isServer()) // for no there is no reason to care about this on the client... // FIXME FABRIC { // FIXME FABRIC final List toDestroy = new ArrayList<>(); // FIXME FABRIC // FIXME FABRIC this.getRepo().updateNetworks(); // FIXME FABRIC for (final Grid g : this.getRepo().networks) { // FIXME FABRIC for (final IGridNode n : g.getNodes()) { // FIXME FABRIC if (n.getWorld() == ev.getWorld()) { // FIXME FABRIC toDestroy.add(n); // FIXME FABRIC } // FIXME FABRIC } // FIXME FABRIC } // FIXME FABRIC // FIXME FABRIC for (final IGridNode n : toDestroy) { // FIXME FABRIC n.destroy(); // FIXME FABRIC } // FIXME FABRIC } // FIXME FABRIC } /** * This is primarily useful for an integrated server being stopped, and can be * fully replaced by the event above once world unload events hit. */ private void onServerStopping(MinecraftServer server) { final List toDestroy = new ArrayList<>(); this.server.updateNetworks(); for (final Grid g : this.server.networks) { for (final IGridNode n : g.getNodes()) { WorldAccess nodeWorld = n.getWorld(); if (nodeWorld instanceof ServerWorld && ((ServerWorld) nodeWorld).getServer() == server) { toDestroy.add(n); } } } for (final IGridNode n : toDestroy) { n.destroy(); } } private void onBeforeWorldTick(ServerWorld world) { final Queue> queue = this.callQueue.get(world); this.processQueue(queue, world); } private void onAfterWorldTick(ServerWorld world) { synchronized (this.craftingJobs) { final Collection jobSet = this.craftingJobs.get(world); if (!jobSet.isEmpty()) { final int jobSize = jobSet.size(); final int microSecondsPerTick = AEConfig.instance().getCraftingCalculationTimePerTick() * 1000; final int simTime = Math.max(1, microSecondsPerTick / jobSize); final Iterator i = jobSet.iterator(); while (i.hasNext()) { final CraftingJob cj = i.next(); if (!cj.simulateFor(simTime)) { i.remove(); } } } } } private void onAfterServerTick(MinecraftServer server) { this.tickColors(this.srvPlayerColors); // ready tiles. final HandlerRep repo = this.getRepo(); while (!repo.tiles.isEmpty()) { final AEBaseBlockEntity bt = repo.tiles.poll(); if (!bt.isRemoved()) { bt.onReady(); } } // tick networks. this.getRepo().updateNetworks(); for (final Grid g : this.getRepo().networks) { g.update(); } // cross world queue. this.processQueue(this.serverQueue, null); } public void registerCraftingSimulation(final World world, final CraftingJob craftingJob) { synchronized (this.craftingJobs) { this.craftingJobs.put(world, craftingJob); } } protected void tickColors(final Map playerSet) { final Iterator i = playerSet.values().iterator(); while (i.hasNext()) { final PlayerColor pc = i.next(); if (pc.ticksLeft <= 0) { i.remove(); } pc.ticksLeft--; } } private void processQueue(final Queue> queue, final World world) { if (queue == null) { return; } final Stopwatch sw = Stopwatch.createStarted(); IWorldCallable c = null; while ((c = queue.poll()) != null) { try { c.call(world); if (sw.elapsed(TimeUnit.MILLISECONDS) > 50) { break; } } catch (final Exception e) { AELog.debug(e); } } } private static class HandlerRep { private Queue tiles = new ArrayDeque<>(); private Set networks = new HashSet<>(); private Set toAdd = new HashSet<>(); private Set toRemove = new HashSet<>(); private void clear() { this.tiles = new ArrayDeque<>(); this.networks = new HashSet<>(); this.toAdd = new HashSet<>(); this.toRemove = new HashSet<>(); } private synchronized void addNetwork(Grid g) { this.toAdd.add(g); this.toRemove.remove(g); } private synchronized void removeNetwork(Grid g) { this.toRemove.add(g); this.toAdd.remove(g); } private synchronized void updateNetworks() { this.networks.removeAll(this.toRemove); this.toRemove.clear(); this.networks.addAll(this.toAdd); this.toAdd.clear(); } } public static class PlayerColor { public final AEColor myColor; private final int myEntity; private int ticksLeft; public PlayerColor(final int id, final AEColor col, final int ticks) { this.myEntity = id; this.myColor = col; this.ticksLeft = ticks; } public PaintedEntityPacket getPacket() { return new PaintedEntityPacket(this.myEntity, this.myColor, this.ticksLeft); } } }