diff --git a/src/main/java/appeng/api/networking/GridServices.java b/src/main/java/appeng/api/networking/GridServices.java
index 5351bd9a8f2..884313a64b0 100644
--- a/src/main/java/appeng/api/networking/GridServices.java
+++ b/src/main/java/appeng/api/networking/GridServices.java
@@ -27,12 +27,16 @@
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.LinkedHashMap;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
+import net.minecraft.world.level.Level;
+
+import appeng.me.helpers.GridServiceContainer;
+
/**
* A registry of grid services to extend grid functionality.
*/
@@ -92,14 +96,37 @@ private static boolean isRegistered(Class> publicInterface) {
*
* This is used by AE2 internally to initialize the services for a grid.
*/
- static Map, IGridServiceProvider> createServices(IGrid g) {
- var result = new LinkedHashMap, IGridServiceProvider>(registry.size());
+ static GridServiceContainer createServices(IGrid g) {
+ var services = new IdentityHashMap, IGridServiceProvider>(registry.size());
+ var serverStartTickServices = new ArrayList(registry.size());
+ var levelStartTickServices = new ArrayList(registry.size());
+ var levelEndTickServices = new ArrayList(registry.size());
+ var serverEndTickServices = new ArrayList(registry.size());
for (var registration : registry) {
- result.put(registration.publicInterface, registration.construct(g, result));
+ var service = registration.construct(g, services);
+ services.put(registration.publicInterface, service);
+
+ if (registration.hasServerStartTick) {
+ serverStartTickServices.add(service);
+ }
+ if (registration.hasLevelStartTick) {
+ levelStartTickServices.add(service);
+ }
+ if (registration.hasLevelEndTick) {
+ levelEndTickServices.add(service);
+ }
+ if (registration.hasServerEndTick) {
+ serverEndTickServices.add(service);
+ }
}
- return result;
+ return new GridServiceContainer(
+ services,
+ serverStartTickServices.toArray(IGridServiceProvider[]::new),
+ levelStartTickServices.toArray(IGridServiceProvider[]::new),
+ levelEndTickServices.toArray(IGridServiceProvider[]::new),
+ serverEndTickServices.toArray(IGridServiceProvider[]::new));
}
private static class GridCacheRegistration {
@@ -114,6 +141,11 @@ private static class GridCacheRegistration {
private final Set> dependencies;
+ private final boolean hasServerStartTick;
+ private final boolean hasLevelStartTick;
+ private final boolean hasLevelEndTick;
+ private final boolean hasServerEndTick;
+
@SuppressWarnings("unchecked")
public GridCacheRegistration(Class implClass, Class> publicInterface) {
this.publicInterface = publicInterface;
@@ -130,6 +162,19 @@ public GridCacheRegistration(Class implClass, Class> publicInterface) {
this.dependencies = Arrays.stream(this.constructorParameterTypes)
.filter(t -> !t.equals(IGrid.class))
.collect(Collectors.toSet());
+
+ try {
+ this.hasServerStartTick = implClass.getMethod("onServerStartTick")
+ .getDeclaringClass() != IGridServiceProvider.class;
+ this.hasLevelStartTick = implClass.getMethod("onLevelStartTick", Level.class)
+ .getDeclaringClass() != IGridServiceProvider.class;
+ this.hasLevelEndTick = implClass.getMethod("onLevelEndTick", Level.class)
+ .getDeclaringClass() != IGridServiceProvider.class;
+ this.hasServerEndTick = implClass.getMethod("onServerEndTick")
+ .getDeclaringClass() != IGridServiceProvider.class;
+ } catch (NoSuchMethodException exception) {
+ throw new RuntimeException("Failed to check which methods the grid service implements", exception);
+ }
}
public IGridServiceProvider construct(IGrid g, Map, IGridServiceProvider> createdServices) {
diff --git a/src/main/java/appeng/api/networking/GridServicesInternal.java b/src/main/java/appeng/api/networking/GridServicesInternal.java
index 1dc48c057a0..1ec2dc67ae4 100644
--- a/src/main/java/appeng/api/networking/GridServicesInternal.java
+++ b/src/main/java/appeng/api/networking/GridServicesInternal.java
@@ -18,14 +18,14 @@
package appeng.api.networking;
-import java.util.Map;
+import appeng.me.helpers.GridServiceContainer;
/**
* Allows access to non-public features of {@link GridServices}.
*/
public class GridServicesInternal {
- public static Map, IGridServiceProvider> createServices(IGrid g) {
+ public static GridServiceContainer createServices(IGrid g) {
return GridServices.createServices(g);
}
diff --git a/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java b/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java
index da0a3878c98..2c881cf1453 100644
--- a/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java
+++ b/src/main/java/appeng/crafting/execution/CraftingCpuLogic.java
@@ -49,6 +49,7 @@
import appeng.core.network.clientbound.CraftingJobStatusPacket;
import appeng.crafting.CraftingLink;
import appeng.crafting.inv.ListCraftingInventory;
+import appeng.hooks.ticking.TickHandler;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import appeng.me.service.CraftingService;
@@ -75,6 +76,8 @@ public class CraftingCpuLogic {
*/
private boolean cantStoreItems = false;
+ private long lastModifiedOnTick = TickHandler.instance().getCurrentTick();
+
public CraftingCpuLogic(CraftingCPUCluster cluster) {
this.cluster = cluster;
}
@@ -390,11 +393,16 @@ public void storeItems() {
}
private void postChange(AEKey what) {
+ lastModifiedOnTick = TickHandler.instance().getCurrentTick();
for (var listener : listeners) {
listener.accept(what);
}
}
+ public long getLastModifiedOnTick() {
+ return lastModifiedOnTick;
+ }
+
public boolean hasJob() {
return this.job != null;
}
@@ -509,6 +517,8 @@ public boolean isCantStoreItems() {
}
private void notifyJobOwner(ExecutingCraftingJob job, CraftingJobStatusPacket.Status status) {
+ this.lastModifiedOnTick = TickHandler.instance().getCurrentTick();
+
var playerId = job.playerId;
if (playerId == null) {
return;
diff --git a/src/main/java/appeng/me/Grid.java b/src/main/java/appeng/me/Grid.java
index a60f4b6de8f..1522869dcb3 100644
--- a/src/main/java/appeng/me/Grid.java
+++ b/src/main/java/appeng/me/Grid.java
@@ -48,7 +48,6 @@
import appeng.api.networking.IGridNode;
import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.IGridService;
-import appeng.api.networking.IGridServiceProvider;
import appeng.api.networking.crafting.ICraftingService;
import appeng.api.networking.energy.IEnergyService;
import appeng.api.networking.events.GridEvent;
@@ -58,6 +57,7 @@
import appeng.api.networking.ticking.ITickManager;
import appeng.core.AELog;
import appeng.hooks.ticking.TickHandler;
+import appeng.me.helpers.GridServiceContainer;
import appeng.me.service.P2PService;
import appeng.parts.AEBasePart;
import appeng.util.IDebugExportable;
@@ -71,7 +71,7 @@ public class Grid implements IGrid {
private static int nextSerial = 0;
private final SetMultimap, IGridNode> machines = MultimapBuilder.hashKeys().hashSetValues().build();
- private final Map, IGridServiceProvider> services;
+ private final GridServiceContainer services;
// Becomes null after the last node has left the grid.
@Nullable
private GridNode pivot;
@@ -103,17 +103,13 @@ int getPriority() {
return this.priority;
}
- Collection getProviders() {
- return this.services.values();
- }
-
@Override
public int size() {
return this.machines.size();
}
void remove(GridNode gridNode) {
- for (var c : this.services.values()) {
+ for (var c : services.services().values()) {
c.removeNode(gridNode);
}
@@ -137,13 +133,13 @@ void add(GridNode gridNode, @Nullable CompoundTag savedData) {
// track node.
this.machines.put(gridNode.getOwner().getClass(), gridNode);
- for (var service : this.services.values()) {
+ for (var service : services.services().values()) {
service.addNode(gridNode, savedData);
}
}
void saveNodeData(GridNode gridNode, CompoundTag savedData) {
- for (var service : this.services.values()) {
+ for (var service : services.services().values()) {
service.saveNodeData(gridNode, savedData);
}
}
@@ -151,7 +147,7 @@ void saveNodeData(GridNode gridNode, CompoundTag savedData) {
@SuppressWarnings("unchecked")
@Override
public C getService(Class iface) {
- var service = this.services.get(iface);
+ var service = this.services.services().get(iface);
if (service == null) {
throw new IllegalArgumentException("Service " + iface + " is not registered");
}
@@ -224,7 +220,7 @@ public void onServerStartTick() {
return;
}
- for (var gc : this.services.values()) {
+ for (var gc : this.services.serverStartTickServices()) {
gc.onServerStartTick();
}
}
@@ -234,7 +230,7 @@ public void onLevelStartTick(Level level) {
return;
}
- for (var gc : this.services.values()) {
+ for (var gc : this.services.levelStartTickServices()) {
gc.onLevelStartTick(level);
}
}
@@ -244,7 +240,7 @@ public void onLevelEndTick(Level level) {
return;
}
- for (var gc : this.services.values()) {
+ for (var gc : this.services.levelEndtickServices()) {
gc.onLevelEndTick(level);
}
}
@@ -254,7 +250,7 @@ public void onServerEndTick() {
return;
}
- for (var gc : this.services.values()) {
+ for (var gc : this.services.serverEndTickServices()) {
gc.onServerEndTick();
}
}
@@ -354,7 +350,7 @@ public void export(JsonWriter jsonWriter) throws IOException {
jsonWriter.name("services");
jsonWriter.beginObject();
- for (var entry : services.entrySet()) {
+ for (var entry : services.services().entrySet()) {
jsonWriter.name(getServiceExportKey(entry.getKey()));
jsonWriter.beginObject();
entry.getValue().debugDump(jsonWriter, registries);
diff --git a/src/main/java/appeng/me/helpers/GridServiceContainer.java b/src/main/java/appeng/me/helpers/GridServiceContainer.java
new file mode 100644
index 00000000000..7c2f17b21ff
--- /dev/null
+++ b/src/main/java/appeng/me/helpers/GridServiceContainer.java
@@ -0,0 +1,13 @@
+package appeng.me.helpers;
+
+import java.util.Map;
+
+import appeng.api.networking.IGridServiceProvider;
+
+public record GridServiceContainer(
+ Map, IGridServiceProvider> services,
+ IGridServiceProvider[] serverStartTickServices,
+ IGridServiceProvider[] levelStartTickServices,
+ IGridServiceProvider[] levelEndtickServices,
+ IGridServiceProvider[] serverEndTickServices) {
+}
diff --git a/src/main/java/appeng/me/service/CraftingService.java b/src/main/java/appeng/me/service/CraftingService.java
index 63ad0f4013b..24ecdd70aa5 100644
--- a/src/main/java/appeng/me/service/CraftingService.java
+++ b/src/main/java/appeng/me/service/CraftingService.java
@@ -71,6 +71,7 @@
import appeng.crafting.CraftingLink;
import appeng.crafting.CraftingLinkNexus;
import appeng.crafting.execution.CraftingSubmitResult;
+import appeng.hooks.ticking.TickHandler;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import appeng.me.helpers.InterestManager;
import appeng.me.helpers.StackWatcher;
@@ -123,11 +124,15 @@ public class CraftingService implements ICraftingService, IGridServiceProvider {
private final IEnergyService energyGrid;
private final Set currentlyCrafting = new HashSet<>();
private final Set currentlyCraftable = new HashSet<>();
+ private long lastProcessedCraftingLogicChangeTick;
+ private long lastProcessedCraftableChangeTick;
private boolean updateList = false;
public CraftingService(IGrid grid, IStorageService storageGrid, IEnergyService energyGrid) {
this.grid = grid;
this.energyGrid = energyGrid;
+ this.lastProcessedCraftingLogicChangeTick = TickHandler.instance().getCurrentTick();
+ this.lastProcessedCraftableChangeTick = TickHandler.instance().getCurrentTick();
storageGrid.addGlobalStorageProvider(new CraftingServiceStorage(this));
}
@@ -137,44 +142,74 @@ public void onServerEndTick() {
if (this.updateList) {
this.updateList = false;
this.updateCPUClusters();
+ lastProcessedCraftingLogicChangeTick = -1; // Ensure caches below are also updated
}
this.craftingLinks.values().removeIf(nexus -> nexus.isDead(this.grid, this));
- var previouslyCrafting = new HashSet<>(currentlyCrafting);
- var previouslyCraftable = new HashSet<>(currentlyCraftable);
- this.currentlyCrafting.clear();
- this.currentlyCraftable.clear();
-
- for (CraftingCPUCluster cpu : this.craftingCPUClusters) {
+ long latestChange = 0;
+ for (var cpu : this.craftingCPUClusters) {
cpu.craftingLogic.tickCraftingLogic(energyGrid, this);
- cpu.craftingLogic.getAllWaitingFor(this.currentlyCrafting);
+ latestChange = Math.max(
+ latestChange,
+ cpu.craftingLogic.getLastModifiedOnTick());
}
- currentlyCraftable.addAll(getCraftables(k -> true));
-
- // Notify watchers about items no longer being crafted
- var changed = new HashSet();
- changed.addAll(Sets.difference(previouslyCrafting, currentlyCrafting));
- changed.addAll(Sets.difference(currentlyCrafting, previouslyCrafting));
- for (var what : changed) {
- for (var watcher : interestManager.get(what)) {
- watcher.getHost().onRequestChange(what);
+
+ // There's nothing to do if we weren't crafting anything and we don't have any CPUs that could craft
+ if (latestChange != lastProcessedCraftingLogicChangeTick) {
+ lastProcessedCraftingLogicChangeTick = latestChange;
+
+ Set previouslyCrafting = currentlyCrafting.isEmpty() ? Set.of() : new HashSet<>(currentlyCrafting);
+ this.currentlyCrafting.clear();
+
+ for (var cpu : this.craftingCPUClusters) {
+ cpu.craftingLogic.getAllWaitingFor(this.currentlyCrafting);
}
- for (var watcher : interestManager.getAllStacksWatchers()) {
- watcher.getHost().onRequestChange(what);
+
+ // Notify watchers about items no longer being crafted, but only if there can be changes and there are
+ // watchers
+ if (!interests.isEmpty() && !(previouslyCrafting.isEmpty() && currentlyCrafting.isEmpty())) {
+ var changed = new HashSet();
+ changed.addAll(Sets.difference(previouslyCrafting, currentlyCrafting));
+ changed.addAll(Sets.difference(currentlyCrafting, previouslyCrafting));
+ for (var what : changed) {
+ for (var watcher : interestManager.get(what)) {
+ watcher.getHost().onRequestChange(what);
+ }
+ for (var watcher : interestManager.getAllStacksWatchers()) {
+ watcher.getHost().onRequestChange(what);
+ }
+ }
}
}
- // Notify watchers about items no longer craftable
- var changedCraftable = new HashSet();
- changedCraftable.addAll(Sets.difference(previouslyCraftable, currentlyCraftable));
- changedCraftable.addAll(Sets.difference(currentlyCraftable, previouslyCraftable));
- for (var what : changedCraftable) {
- for (var watcher : interestManager.get(what)) {
- watcher.getHost().onCraftableChange(what);
- }
- for (var watcher : interestManager.getAllStacksWatchers()) {
- watcher.getHost().onCraftableChange(what);
+ // Throttle updates of craftables to once every 10 ticks
+ if (lastProcessedCraftableChangeTick != craftingProviders.getLastModifiedOnTick()) {
+ lastProcessedCraftableChangeTick = craftingProviders.getLastModifiedOnTick();
+
+ // If everything is empty, there's nothing to do
+ if (!currentlyCraftable.isEmpty() || !craftingProviders.getCraftableKeys().isEmpty()
+ || !craftingProviders.getEmittableKeys().isEmpty()) {
+ Set previouslyCraftable = currentlyCraftable.isEmpty() ? Set.of()
+ : new HashSet<>(currentlyCraftable);
+ this.currentlyCraftable.clear();
+ currentlyCraftable.addAll(craftingProviders.getCraftableKeys());
+ currentlyCraftable.addAll(craftingProviders.getEmittableKeys());
+
+ // Only perform the change tracking if there are watchers
+ if (!interests.isEmpty()) {
+ var changedCraftable = new HashSet();
+ changedCraftable.addAll(Sets.difference(previouslyCraftable, currentlyCraftable));
+ changedCraftable.addAll(Sets.difference(currentlyCraftable, previouslyCraftable));
+ for (var what : changedCraftable) {
+ for (var watcher : interestManager.get(what)) {
+ watcher.getHost().onCraftableChange(what);
+ }
+ for (var watcher : interestManager.getAllStacksWatchers()) {
+ watcher.getHost().onCraftableChange(what);
+ }
+ }
+ }
}
}
}
diff --git a/src/main/java/appeng/me/service/TickManagerService.java b/src/main/java/appeng/me/service/TickManagerService.java
index 09cf496cdf7..554b628d47e 100644
--- a/src/main/java/appeng/me/service/TickManagerService.java
+++ b/src/main/java/appeng/me/service/TickManagerService.java
@@ -18,7 +18,7 @@
package appeng.me.service;
-import java.util.HashMap;
+import java.util.IdentityHashMap;
import java.util.LongSummaryStatistics;
import java.util.Map;
import java.util.Objects;
@@ -50,10 +50,10 @@ public class TickManagerService implements ITickManager, IGridServiceProvider {
private static final int TICK_RATE_SPEED_UP_FACTOR = 2;
private static final int TICK_RATE_SLOW_DOWN_FACTOR = 1;
- private final Map alertable = new HashMap<>();
- private final Map sleeping = new HashMap<>();
- private final Map awake = new HashMap<>();
- private final Map> upcomingTicks = new HashMap<>();
+ private final Map alertable = new IdentityHashMap<>();
+ private final Map sleeping = new IdentityHashMap<>();
+ private final Map awake = new IdentityHashMap<>();
+ private final Map> upcomingTicks = new IdentityHashMap<>();
private PriorityQueue currentlyTickingQueue = null;
@@ -70,10 +70,6 @@ public void onServerStartTick() {
this.currentTick++;
}
- @Override
- public void onLevelStartTick(Level level) {
- }
-
@Override
public void onLevelEndTick(Level level) {
this.tickLevelQueue(level);
diff --git a/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java b/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java
index 0ab6b33dca3..5bfabc0697d 100644
--- a/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java
+++ b/src/main/java/appeng/me/service/helpers/NetworkCraftingProviders.java
@@ -23,6 +23,7 @@
import appeng.api.stacks.AEKey;
import appeng.api.stacks.KeyCounter;
import appeng.api.storage.AEKeyFilter;
+import appeng.hooks.ticking.TickHandler;
/**
* Keeps track of the crafting patterns in the network, and related information.
@@ -37,6 +38,11 @@ public class NetworkCraftingProviders {
private final KeyCounter craftableItemsList = new KeyCounter();
private final Map emitableItems = new HashMap<>();
+ private final Set craftableKeys = Collections.unmodifiableSet(craftableItems.keySet());
+ private final Set emittableKeys = Collections.unmodifiableSet(emitableItems.keySet());
+
+ private long lastModifiedOnTick = TickHandler.instance().getCurrentTick();
+
public void addProvider(IGridNode node) {
var provider = node.getService(ICraftingProvider.class);
if (provider != null) {
@@ -46,6 +52,7 @@ public void addProvider(IGridNode node) {
var state = new ProviderState(provider);
state.mount(this);
craftingProviders.put(node, state);
+ setLastModifiedOnTick();
}
}
@@ -55,6 +62,7 @@ public void removeProvider(IGridNode node) {
var state = craftingProviders.remove(node);
if (state != null) {
state.unmount(this);
+ setLastModifiedOnTick();
}
}
}
@@ -78,6 +86,14 @@ public Set getCraftables(AEKeyFilter filter) {
return result;
}
+ public Set getCraftableKeys() {
+ return craftableKeys;
+ }
+
+ public Set getEmittableKeys() {
+ return emittableKeys;
+ }
+
public Collection getCraftingFor(AEKey whatToCraft) {
var patterns = this.craftableItems.get(whatToCraft);
if (patterns != null) {
@@ -208,4 +224,15 @@ private List getSortedPatterns() {
private record PatternInfo(IPatternDetails pattern, ProviderState state) {
}
+
+ private void setLastModifiedOnTick() {
+ lastModifiedOnTick = TickHandler.instance().getCurrentTick();
+ }
+
+ /**
+ * @see TickHandler#getCurrentTick()
+ */
+ public long getLastModifiedOnTick() {
+ return lastModifiedOnTick;
+ }
}
diff --git a/src/test/java/appeng/api/networking/GridServicesTest.java b/src/test/java/appeng/api/networking/GridServicesTest.java
index 9d625a1e583..a5145b263f3 100644
--- a/src/test/java/appeng/api/networking/GridServicesTest.java
+++ b/src/test/java/appeng/api/networking/GridServicesTest.java
@@ -30,6 +30,8 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
+import net.minecraft.world.level.Level;
+
import appeng.util.BootstrapMinecraft;
@MockitoSettings
@@ -62,7 +64,7 @@ void restoreRegistry() throws Exception {
@Test
void testEmptyRegistry() {
- var services = GridServices.createServices(grid);
+ var services = GridServices.createServices(grid).services();
assertThat(services).isEmpty();
}
@@ -70,7 +72,7 @@ void testEmptyRegistry() {
void testGridServiceWithDefaultConstructor() {
GridServices.register(PublicInterface.class, GridService1.class);
- var services = GridServices.createServices(grid);
+ var services = GridServices.createServices(grid).services();
assertThat(services).containsOnlyKeys(PublicInterface.class);
assertThat(services.get(PublicInterface.class)).isInstanceOf(GridService1.class);
}
@@ -79,7 +81,7 @@ void testGridServiceWithDefaultConstructor() {
void testGridServiceWithGridDependency() {
GridServices.register(GridService2.class, GridService2.class);
- var services = GridServices.createServices(grid);
+ var services = GridServices.createServices(grid).services();
assertThat(services).containsOnlyKeys(GridService2.class);
var actual = (GridService2) services.get(GridService2.class);
assertThat(actual.grid).isSameAs(grid);
@@ -92,7 +94,7 @@ void testGridServicesWithDependencies() {
GridServices.register(GridService3.class, GridService3.class);
GridServices.register(GridService4.class, GridService4.class);
- var services = GridServices.createServices(grid);
+ var services = GridServices.createServices(grid).services();
assertThat(services).containsOnlyKeys(
PublicInterface.class,
GridService2.class,
@@ -176,4 +178,50 @@ public AmbiguousConstructorClass(GridService2 gc2) {
}
}
+ @Test
+ void testTickingServices() {
+ GridServices.register(ServerStartTickOnly.class, ServerStartTickOnly.class);
+ GridServices.register(LevelStartTickOnly.class, LevelStartTickOnly.class);
+ GridServices.register(LevelEndTickOnly.class, LevelEndTickOnly.class);
+ GridServices.register(ServerEndTickOnly.class, ServerEndTickOnly.class);
+
+ var services = GridServicesInternal.createServices(grid);
+ var map = services.services();
+ assertThat(services.serverStartTickServices())
+ .containsExactly(map.get(ServerStartTickOnly.class));
+ assertThat(services.levelStartTickServices())
+ .containsExactly(map.get(LevelStartTickOnly.class));
+ assertThat(services.levelEndtickServices())
+ .containsExactly(map.get(LevelEndTickOnly.class));
+ assertThat(services.serverEndTickServices())
+ .containsExactly(map.get(ServerEndTickOnly.class));
+ }
+
+ public static class ServerStartTickOnly implements IGridServiceProvider {
+ @Override
+ public void onServerStartTick() {
+ IGridServiceProvider.super.onServerStartTick();
+ }
+ }
+
+ public static class LevelStartTickOnly implements IGridServiceProvider {
+ @Override
+ public void onLevelStartTick(Level level) {
+ IGridServiceProvider.super.onLevelStartTick(level);
+ }
+ }
+
+ public static class LevelEndTickOnly implements IGridServiceProvider {
+ @Override
+ public void onLevelEndTick(Level level) {
+ IGridServiceProvider.super.onLevelEndTick(level);
+ }
+ }
+
+ public static class ServerEndTickOnly implements IGridServiceProvider {
+ @Override
+ public void onServerEndTick() {
+ IGridServiceProvider.super.onServerEndTick();
+ }
+ }
}