diff --git a/src/main/java/emu/grasscutter/server/event/game/ServerTickEvent.java b/src/main/java/emu/grasscutter/server/event/game/ServerTickEvent.java index d1c12d02d..612ba26d2 100644 --- a/src/main/java/emu/grasscutter/server/event/game/ServerTickEvent.java +++ b/src/main/java/emu/grasscutter/server/event/game/ServerTickEvent.java @@ -1,9 +1,33 @@ package emu.grasscutter.server.event.game; import emu.grasscutter.server.event.types.ServerEvent; +import java.time.Instant; + +import java.time.Instant; public final class ServerTickEvent extends ServerEvent { - public ServerTickEvent() { + private final Instant start, end; + + public ServerTickEvent(Instant start, Instant end) { super(Type.GAME); + + this.start = start; + this.end = end; } -} + + public Instant getTickStart() { + return this.start; + } + + public Instant getTickEnd() { + return this.end; + } + + public Instant getTickStart() { + return this.start; + } + + public Instant getTickEnd() { + return this.end; + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 1218bd67b..76eedbc51 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -26,11 +26,14 @@ import emu.grasscutter.server.event.types.ServerEvent; import emu.grasscutter.server.event.game.ServerTickEvent; import emu.grasscutter.server.event.internal.ServerStartEvent; import emu.grasscutter.server.event.internal.ServerStopEvent; +import emu.grasscutter.server.scheduler.ServerTaskScheduler; import emu.grasscutter.task.TaskMap; import kcp.highway.ChannelConfig; import kcp.highway.KcpServer; +import lombok.Getter; import java.net.InetSocketAddress; +import java.time.Instant; import java.time.OffsetDateTime; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -42,10 +45,11 @@ public final class GameServer extends KcpServer { private final InetSocketAddress address; private final GameServerPacketHandler packetHandler; private final ServerQuestHandler questHandler; - + @Getter private final ServerTaskScheduler scheduler; + private final Map players; private final Set worlds; - + private ChatManagerHandler chatManager; private final InventoryManager inventoryManager; private final GachaManager gachaManager; @@ -57,14 +61,14 @@ public final class GameServer extends KcpServer { private final TaskMap taskMap; private final DropManager dropManager; private final WorldDataManager worldDataManager; - + private final CombineManger combineManger; private final TowerScheduleManager towerScheduleManager; public GameServer() { this(getAdapterInetSocketAddress()); } - + public GameServer(InetSocketAddress address) { ChannelConfig channelConfig = new ChannelConfig(); channelConfig.nodelay(true,40,2,true); @@ -80,9 +84,10 @@ public final class GameServer extends KcpServer { this.address = address; this.packetHandler = new GameServerPacketHandler(PacketHandler.class); this.questHandler = new ServerQuestHandler(); + this.scheduler = new ServerTaskScheduler(); this.players = new ConcurrentHashMap<>(); this.worlds = Collections.synchronizedSet(new HashSet<>()); - + this.chatManager = new ChatManager(this); this.inventoryManager = new InventoryManager(this); this.gachaManager = new GachaManager(this); @@ -99,7 +104,7 @@ public final class GameServer extends KcpServer { // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); } - + public GameServerPacketHandler getPacketHandler() { return packetHandler; } @@ -119,7 +124,7 @@ public final class GameServer extends KcpServer { public ChatManagerHandler getChatManager() { return chatManager; } - + public void setChatManager(ChatManagerHandler chatManager) { this.chatManager = chatManager; } @@ -131,7 +136,7 @@ public final class GameServer extends KcpServer { public GachaManager getGachaManager() { return gachaManager; } - + public ShopManager getShopManager() { return shopManager; } @@ -143,7 +148,7 @@ public final class GameServer extends KcpServer { public DropManager getDropManager() { return dropManager; } - + public DungeonManager getDungeonManager() { return dungeonManager; } @@ -171,7 +176,7 @@ public final class GameServer extends KcpServer { public TaskMap getTaskMap() { return this.taskMap; } - + private static InetSocketAddress getAdapterInetSocketAddress(){ InetSocketAddress inetSocketAddress; if(GAME_INFO.bindAddress.equals("")){ @@ -184,7 +189,7 @@ public final class GameServer extends KcpServer { } return inetSocketAddress; } - + public void registerPlayer(Player player) { getPlayers().put(player.getUid(), player); } @@ -192,44 +197,44 @@ public final class GameServer extends KcpServer { public Player getPlayerByUid(int id) { return this.getPlayerByUid(id, false); } - + public Player getPlayerByUid(int id, boolean allowOfflinePlayers) { // Console check if (id == GameConstants.SERVER_CONSOLE_UID) { return null; } - + // Get from online players Player player = this.getPlayers().get(id); - + if (!allowOfflinePlayers) { return player; } - + // Check database if character isnt here if (player == null) { player = DatabaseHelper.getPlayerByUid(id); } - + return player; } - + public Player getPlayerByAccountId(String accountId) { Optional playerOpt = getPlayers().values().stream().filter(player -> player.getAccount().getId().equals(accountId)).findFirst(); return playerOpt.orElse(null); } - + public SocialDetail.Builder getSocialDetailByUid(int id) { // Get from online players Player player = this.getPlayerByUid(id, true); - + if (player == null) { return null; } - + return player.getSocialDetail(); } - + public Account getAccountByName(String username) { Optional playerOpt = getPlayers().values().stream().filter(player -> player.getAccount().getUsername().equals(username)).findFirst(); if (playerOpt.isPresent()) { @@ -238,32 +243,41 @@ public final class GameServer extends KcpServer { return DatabaseHelper.getAccountByName(username); } - public synchronized void onTick(){ - Iterator it = this.getWorlds().iterator(); - while (it.hasNext()) { - World world = it.next(); + public synchronized void onTick() { + var tickStart = Instant.now(); - if (world.getPlayerCount() == 0) { - it.remove(); - } + // Tick worlds. + Iterator it = this.getWorlds().iterator(); + while (it.hasNext()) { + World world = it.next(); - world.onTick(); - } + if (world.getPlayerCount() == 0) { + it.remove(); + } - for (Player player : this.getPlayers().values()) { - player.onTick(); - } + world.onTick(); + } + + // Tick players. + for (Player player : this.getPlayers().values()) { + player.onTick(); + } + + // Tick scheduler. + this.getScheduler().runTasks(); + + // Call server tick event. + ServerTickEvent event = new ServerTickEvent(tickStart, Instant.now()); + event.call(); + } - ServerTickEvent event = new ServerTickEvent(); event.call(); - } - public void registerWorld(World world) { this.getWorlds().add(world); } - + public void deregisterWorld(World world) { // TODO Auto-generated method stub - + } public void start() { diff --git a/src/main/java/emu/grasscutter/server/scheduler/AsyncServerTask.java b/src/main/java/emu/grasscutter/server/scheduler/AsyncServerTask.java new file mode 100644 index 000000000..b44e614a4 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/scheduler/AsyncServerTask.java @@ -0,0 +1,98 @@ +package emu.grasscutter.server.scheduler; + +import lombok.Getter; + +import javax.annotation.Nullable; + +/** + * A server task that should be run asynchronously. + */ +public final class AsyncServerTask implements Runnable { + /* The runnable to run. */ + private final Runnable task; + /* This ID is assigned by the scheduler. */ + @Getter private final int taskId; + /* The result callback to run. */ + @Nullable private final Runnable callback; + + /* Has the task already been started? */ + private boolean started = false; + /* Has the task finished execution? */ + private boolean finished = false; + /* The result produced in the async task. */ + @Nullable private Object result = null; + + /** + * For tasks without a callback. + * @param task The task to run. + */ + public AsyncServerTask(Runnable task, int taskId) { + this(task, null, taskId); + } + + /** + * For tasks with a callback. + * @param task The task to run. + * @param callback The task to run after the task is complete. + */ + public AsyncServerTask(Runnable task, @Nullable Runnable callback, int taskId) { + this.task = task; + this.callback = callback; + this.taskId = taskId; + } + + /** + * Returns the state of the task. + * @return True if the task has been started, false otherwise. + */ + public boolean hasStarted() { + return this.started; + } + + /** + * Returns the state of the task. + * @return True if the task has finished execution, false otherwise. + */ + public boolean isFinished() { + return this.finished; + } + + /** + * Runs the task. + */ + @Override public void run() { + // Declare the task as started. + this.started = true; + + // Run the runnable. + this.task.run(); + + // Declare the task as finished. + this.finished = true; + } + + /** + * Runs the callback. + */ + public void complete() { + // Run the callback. + if(this.callback != null) + this.callback.run(); + } + + /** + * Sets the result of the async task. + * @param result The result of the async task. + */ + public void setResult(@Nullable Object result) { + this.result = result; + } + + /** + * Returns the set result of the async task. + * @return The result, or null if it has not been set. + */ + @Nullable public Object getResult() { + return this.result; + } +} diff --git a/src/main/java/emu/grasscutter/server/scheduler/ServerTask.java b/src/main/java/emu/grasscutter/server/scheduler/ServerTask.java new file mode 100644 index 000000000..0b80547b8 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/scheduler/ServerTask.java @@ -0,0 +1,67 @@ +package emu.grasscutter.server.scheduler; + +import emu.grasscutter.Grasscutter; +import lombok.*; + +/** + * This class works the same as a runnable, except with more information. + */ +public final class ServerTask implements Runnable { + /* The runnable to run. */ + private final Runnable runnable; + /* This ID is assigned by the scheduler. */ + @Getter private final int taskId; + /* The period at which the task should be run. */ + /* The delay between the first execute. */ + private final int period, delay; + + public ServerTask(Runnable runnable, int taskId, int period, int delay) { + this.runnable = runnable; + this.taskId = taskId; + this.period = period; + this.delay = delay; + } + + /* The amount of times the task has been run. */ + @Getter private int ticks = 0; + /* Should the check consider delay? */ + private boolean considerDelay = true; + + /** + * Cancels the task from running the next time. + */ + public void cancel() { + Grasscutter.getGameServer().getScheduler().cancelTask(this.taskId); + } + + /** + * Checks if the task should run at the current tick. + * @return True if the task should run, false otherwise. + */ + public boolean shouldRun() { + if(this.delay != -1 && this.considerDelay) { + this.considerDelay = false; + return this.ticks == this.delay; + } else if(this.period != -1) + return this.ticks % this.period == 0; + else return true; + } + + /** + * Checks if the task should be canceled. + * @return True if the task should be canceled, false otherwise. + */ + public boolean shouldCancel() { + return this.period == -1; + } + + /** + * Runs the task. + */ + @Override public void run() { + // Run the runnable. + this.runnable.run(); + // Increase tick count. + this.ticks++; + } +} \ No newline at end of file diff --git a/src/main/java/emu/grasscutter/server/scheduler/ServerTaskScheduler.java b/src/main/java/emu/grasscutter/server/scheduler/ServerTaskScheduler.java new file mode 100644 index 000000000..f9bac4439 --- /dev/null +++ b/src/main/java/emu/grasscutter/server/scheduler/ServerTaskScheduler.java @@ -0,0 +1,148 @@ +package emu.grasscutter.server.scheduler; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * A class to manage all time-based tasks scheduled on the server. + * This handles both synchronous and asynchronous tasks. + * + * Developers note: A server tick is ONE REAL-TIME SECOND. + */ +public final class ServerTaskScheduler { + /* A map to contain all running tasks. */ + private final ConcurrentHashMap tasks + = new ConcurrentHashMap<>(); + /* A map to contain all async tasks. */ + private final ConcurrentHashMap asyncTasks + = new ConcurrentHashMap<>(); + + /* The ID assigned to the next runnable. */ + private int nextTaskId = 0; + + /** + * Ran every server tick. + * Attempts to run all scheduled tasks. + * This method is synchronous and will block until all tasks are complete. + */ + public void runTasks() { + // Skip if there are no tasks. + if(this.tasks.size() == 0) + return; + + // Run all tasks. + for(ServerTask task : this.tasks.values()) { + // Check if the task should run. + if (task.shouldRun()) { + // Run the task. + task.run(); + } + + // Check if the task should be canceled. + if (task.shouldCancel()) { + // Cancel the task. + this.cancelTask(task.getTaskId()); + } + } + + // Run all async tasks. + for(AsyncServerTask task : this.asyncTasks.values()) { + if(!task.hasStarted()) { + // Create a thread for the task. + Thread thread = new Thread(task); + // Start the thread. + thread.start(); + } else if(task.isFinished()) { + // Cancel the task. + this.asyncTasks.remove(task.getTaskId()); + // Run the task's callback. + task.complete(); + } + } + } + + /** + * Gets a task from the scheduler. + * @param taskId The ID of the task to get. + * @return The task, or null if it does not exist. + */ + public ServerTask getTask(int taskId) { + return this.tasks.get(taskId); + } + + /** + * Gets an async task from the scheduler. + * @param taskId The ID of the task to get. + * @return The task, or null if it does not exist. + */ + public AsyncServerTask getAsyncTask(int taskId) { + return this.asyncTasks.get(taskId); + } + + /** + * Removes a task from the scheduler. + * @param taskId The ID of the task to remove. + */ + public void cancelTask(int taskId) { + this.tasks.remove(taskId); + } + + /** + * Schedules a task to be run on a separate thread. + * The task runs on the next server tick. + * @param runnable The runnable to run. + * @return The ID of the task. + */ + public int scheduleAsyncTask(Runnable runnable) { + // Get the next task ID. + var taskId = this.nextTaskId++; + // Create a new task. + this.asyncTasks.put(taskId, new AsyncServerTask(runnable, taskId)); + // Return the task ID. + return taskId; + } + + /** + * Schedules a task to be run on the next server tick. + * @param runnable The runnable to run. + * @return The ID of the task. + */ + public int scheduleTask(Runnable runnable) { + return this.scheduleDelayedRepeatingTask(runnable, -1, -1); + } + + /** + * Schedules a task to be run after the amount of ticks has passed. + * @param runnable The runnable to run. + * @param delay The amount of ticks to wait before running. + * @return The ID of the task. + */ + public int scheduleDelayedTask(Runnable runnable, int delay) { + return this.scheduleDelayedRepeatingTask(runnable, -1, delay); + } + + /** + * Schedules a task to be run every amount of ticks. + * @param runnable The runnable to run. + * @param period The amount of ticks to wait before running again. + * @return The ID of the task. + */ + public int scheduleRepeatingTask(Runnable runnable, int period) { + return this.scheduleDelayedRepeatingTask(runnable, period, 0); + } + + /** + * Schedules a task to be run after the amount of ticks has passed. + * @param runnable The runnable to run. + * @param period The amount of ticks to wait before running again. + * @param delay The amount of ticks to wait before running the first time. + * @return The ID of the task. + */ + public int scheduleDelayedRepeatingTask(Runnable runnable, int period, int delay) { + // Get the next task ID. + var taskId = this.nextTaskId++; + // Create a new task. + this.tasks.put(taskId, new ServerTask(runnable, taskId, period, delay)); + // Return the task ID. + return taskId; + } +} \ No newline at end of file