diff --git a/Arrowgene.Ddon.Database/Arrowgene.Ddon.Database.csproj b/Arrowgene.Ddon.Database/Arrowgene.Ddon.Database.csproj
index 204769318..f10daa3bb 100644
--- a/Arrowgene.Ddon.Database/Arrowgene.Ddon.Database.csproj
+++ b/Arrowgene.Ddon.Database/Arrowgene.Ddon.Database.csproj
@@ -41,5 +41,8 @@
PreserveNewest
+
+ PreserveNewest
+
diff --git a/Arrowgene.Ddon.Database/DdonDatabaseBuilder.cs b/Arrowgene.Ddon.Database/DdonDatabaseBuilder.cs
index 11f96110e..507c5bd39 100644
--- a/Arrowgene.Ddon.Database/DdonDatabaseBuilder.cs
+++ b/Arrowgene.Ddon.Database/DdonDatabaseBuilder.cs
@@ -14,7 +14,7 @@ public static class DdonDatabaseBuilder
private static readonly ILogger Logger = LogProvider.Logger(typeof(DdonDatabaseBuilder));
private const string DefaultSchemaFile = "Script/schema_sqlite.sql";
- public const uint Version = 26;
+ public const uint Version = 27;
public static IDatabase Build(DatabaseSetting settings)
{
diff --git a/Arrowgene.Ddon.Database/Files/Database/Script/migration_scheduling.sql b/Arrowgene.Ddon.Database/Files/Database/Script/migration_scheduling.sql
new file mode 100644
index 000000000..c39ec37ca
--- /dev/null
+++ b/Arrowgene.Ddon.Database/Files/Database/Script/migration_scheduling.sql
@@ -0,0 +1,7 @@
+CREATE TABLE ddon_schedule_next (
+ "type" INTEGER NOT NULL,
+ "timestamp" BIGINT NOT NULL,
+ PRIMARY KEY("type")
+);
+
+INSERT INTO ddon_schedule_next(type, timestamp) VALUES (19, 0);
diff --git a/Arrowgene.Ddon.Database/Files/Database/Script/schema_sqlite.sql b/Arrowgene.Ddon.Database/Files/Database/Script/schema_sqlite.sql
index 99e5ba454..1a898d2bb 100644
--- a/Arrowgene.Ddon.Database/Files/Database/Script/schema_sqlite.sql
+++ b/Arrowgene.Ddon.Database/Files/Database/Script/schema_sqlite.sql
@@ -782,3 +782,11 @@ CREATE TABLE IF NOT EXISTS ddon_epitaph_claimed_weekly_rewards (
CONSTRAINT "pk_ddon_epitaph_claimed_weekly_rewards" PRIMARY KEY ("character_id", "epitaph_id"),
CONSTRAINT "fk_ddon_epitaph_claimed_weekly_rewards_character_id" FOREIGN KEY ("character_id") REFERENCES "ddon_character"("character_id") ON DELETE CASCADE
);
+
+
+CREATE TABLE IF NOT EXISTS ddon_schedule_next (
+ "type" INTEGER NOT NULL,
+ "timestamp" BIGINT NOT NULL,
+ PRIMARY KEY("type")
+);
+INSERT INTO ddon_schedule_next(type, timestamp) VALUES (19, 0);
diff --git a/Arrowgene.Ddon.Database/IDatabase.cs b/Arrowgene.Ddon.Database/IDatabase.cs
index cdac143c0..a932be218 100644
--- a/Arrowgene.Ddon.Database/IDatabase.cs
+++ b/Arrowgene.Ddon.Database/IDatabase.cs
@@ -11,6 +11,7 @@
using Arrowgene.Ddon.Shared.Model.BattleContent;
using Arrowgene.Ddon.Shared.Model.Clan;
using Arrowgene.Ddon.Shared.Model.Quest;
+using Arrowgene.Ddon.Shared.Model.Scheduler;
namespace Arrowgene.Ddon.Database
{
@@ -590,6 +591,10 @@ bool InsertBBMContentTreasure(
bool InsertEpitaphWeeklyReward(uint characterId, uint epitaphId, DbConnection? connectionIn = null);
HashSet GetEpitaphClaimedWeeklyRewards(uint characterId, DbConnection? connectionIn = null);
- void DeleteWeeklyRewards(DbConnection? connectionIn = null);
+ void DeleteWeeklyEpitaphClaimedRewards(DbConnection? connectionIn = null);
+
+ // Scheduler
+ Dictionary SelectAllTaskEntries();
+ bool UpdateScheduleInfo(TaskType type, long timestamp);
}
}
diff --git a/Arrowgene.Ddon.Database/Sql/Core/DdonSqlDbEpitaphRoadClaimedWeeklyRewards.cs b/Arrowgene.Ddon.Database/Sql/Core/DdonSqlDbEpitaphRoadClaimedWeeklyRewards.cs
index c9626a4b9..ce715f5bd 100644
--- a/Arrowgene.Ddon.Database/Sql/Core/DdonSqlDbEpitaphRoadClaimedWeeklyRewards.cs
+++ b/Arrowgene.Ddon.Database/Sql/Core/DdonSqlDbEpitaphRoadClaimedWeeklyRewards.cs
@@ -49,7 +49,7 @@ public HashSet GetEpitaphClaimedWeeklyRewards(uint characterId, DbConnecti
return results;
}
- public void DeleteWeeklyRewards(DbConnection? connectionIn = null)
+ public void DeleteWeeklyEpitaphClaimedRewards(DbConnection? connectionIn = null)
{
ExecuteQuerySafe(connectionIn, (connection) =>
{
diff --git a/Arrowgene.Ddon.Database/Sql/Core/DdonSqlDbScheduleNext.cs b/Arrowgene.Ddon.Database/Sql/Core/DdonSqlDbScheduleNext.cs
new file mode 100644
index 000000000..677d655e3
--- /dev/null
+++ b/Arrowgene.Ddon.Database/Sql/Core/DdonSqlDbScheduleNext.cs
@@ -0,0 +1,49 @@
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+using System.Collections.Generic;
+using System.Data.Common;
+
+namespace Arrowgene.Ddon.Database.Sql.Core
+{
+ public abstract partial class DdonSqlDb : SqlDb
+ where TCon : DbConnection
+ where TCom : DbCommand
+ where TReader : DbDataReader
+ {
+ protected static readonly string[] ScheduleNextFields = new string[]
+ {
+ "type", "timestamp"
+ };
+
+ private static readonly string SqlUpdateScheduleNext = $"UPDATE \"ddon_schedule_next\" SET \"timestamp\"=@timestamp WHERE \"type\"=@type;";
+ private static readonly string SqlSelectScheduleNext = $"SELECT {BuildQueryField(ScheduleNextFields)} FROM \"ddon_schedule_next\";";
+
+
+ public Dictionary SelectAllTaskEntries()
+ {
+ Dictionary results = new Dictionary();
+ ExecuteReader(SqlSelectScheduleNext, command => { }, reader =>
+ {
+ while (reader.Read())
+ {
+ TaskType type = (TaskType) GetUInt32(reader, "type");
+ results[type] = new SchedulerTaskEntry()
+ {
+ Type = type,
+ Timestamp = GetInt64(reader, "timestamp")
+ };
+ }
+ });
+ return results;
+ }
+
+ public bool UpdateScheduleInfo(TaskType type, long timestamp)
+ {
+ return ExecuteNonQuery(SqlUpdateScheduleNext, command =>
+ {
+ AddParameter(command, "@type", (uint) type);
+ AddParameter(command, "@timestamp", timestamp);
+ }) == 1;
+ }
+ }
+}
+
diff --git a/Arrowgene.Ddon.Database/Sql/Core/Migration/00000026_TaskSchedulerMigration.cs b/Arrowgene.Ddon.Database/Sql/Core/Migration/00000026_TaskSchedulerMigration.cs
new file mode 100644
index 000000000..d99366b89
--- /dev/null
+++ b/Arrowgene.Ddon.Database/Sql/Core/Migration/00000026_TaskSchedulerMigration.cs
@@ -0,0 +1,25 @@
+using System.Data.Common;
+
+namespace Arrowgene.Ddon.Database.Sql.Core.Migration
+{
+ public class TaskSchedulerMigration : IMigrationStrategy
+ {
+ public uint From => 26;
+ public uint To => 27;
+
+ private readonly DatabaseSetting DatabaseSetting;
+
+ public TaskSchedulerMigration(DatabaseSetting databaseSetting)
+ {
+ DatabaseSetting = databaseSetting;
+ }
+
+ public bool Migrate(IDatabase db, DbConnection conn)
+ {
+ string adaptedSchema = DdonDatabaseBuilder.GetAdaptedSchema(DatabaseSetting, "Script/migration_scheduling.sql");
+ db.Execute(conn, adaptedSchema);
+ return true;
+ }
+ }
+}
+
diff --git a/Arrowgene.Ddon.GameServer/Characters/CharacterManager.cs b/Arrowgene.Ddon.GameServer/Characters/CharacterManager.cs
index 247655845..cb3e1c3c8 100644
--- a/Arrowgene.Ddon.GameServer/Characters/CharacterManager.cs
+++ b/Arrowgene.Ddon.GameServer/Characters/CharacterManager.cs
@@ -61,7 +61,7 @@ public Character SelectCharacter(uint characterId, DbConnection? connectionIn =
character.EpitaphRoadState.UnlockedContent = _Server.Database.GetEpitaphRoadUnlocks(character.CharacterId, connectionIn);
- if (_Server.Setting.GameLogicSetting.EnableEpitaphWeeklyRewards)
+ if (_Server.Setting.GameLogicSetting.EnableEpitaphWeeklyRewards.Value)
{
character.EpitaphRoadState.WeeklyRewardsClaimed = _Server.Database.GetEpitaphClaimedWeeklyRewards(character.CharacterId, connectionIn);
}
diff --git a/Arrowgene.Ddon.GameServer/Characters/EpitaphRoadManager.cs b/Arrowgene.Ddon.GameServer/Characters/EpitaphRoadManager.cs
index cb280f5e8..34c959d00 100644
--- a/Arrowgene.Ddon.GameServer/Characters/EpitaphRoadManager.cs
+++ b/Arrowgene.Ddon.GameServer/Characters/EpitaphRoadManager.cs
@@ -1290,7 +1290,7 @@ public List RollGatheringLoot(GameClient client, Charact
{
results.AddRange(RollWeeklyChestReward(dungeonInfo, reward));
- if (_Server.Setting.GameLogicSetting.EnableEpitaphWeeklyRewards)
+ if (_Server.Setting.GameLogicSetting.EnableEpitaphWeeklyRewards.Value)
{
character.EpitaphRoadState.WeeklyRewardsClaimed.Add(reward.EpitaphId);
_Server.Database.InsertEpitaphWeeklyReward(character.CharacterId, reward.EpitaphId);
@@ -1321,5 +1321,22 @@ public List RollGatheringLoot(GameClient client, Charact
return results;
}
+
+ ///
+ /// Called by the task manager. The main task will signal all channels
+ /// to flush the cached information queried by the player when first
+ /// logging in and send a notification to all players that the action
+ /// occurred.
+ ///
+ public void PerformWeeklyReset()
+ {
+ _Server.ChatManager.BroadcastMessage(LobbyChatMsgType.ManagementAlertN, "Epitaph Road Weekly Rewards Reset");
+
+ // Clear out cached data related to epitaph weekly rewards
+ foreach (var client in _Server.ClientLookup.GetAll())
+ {
+ client.Character.EpitaphRoadState.WeeklyRewardsClaimed.Clear();
+ }
+ }
}
}
diff --git a/Arrowgene.Ddon.GameServer/Chat/ChatManager.cs b/Arrowgene.Ddon.GameServer/Chat/ChatManager.cs
index 37d0ac28f..94f2e82ec 100644
--- a/Arrowgene.Ddon.GameServer/Chat/ChatManager.cs
+++ b/Arrowgene.Ddon.GameServer/Chat/ChatManager.cs
@@ -4,6 +4,7 @@
using Arrowgene.Ddon.Shared.Entity.Structure;
using Arrowgene.Ddon.Shared.Model;
using Arrowgene.Logging;
+using System;
using System.Collections.Generic;
using System.Linq;
@@ -14,11 +15,11 @@ public class ChatManager
private static readonly ServerLogger Logger = LogProvider.Logger(typeof(ChatManager));
private readonly List _handler;
- private readonly DdonGameServer _server;
+ private readonly DdonGameServer _Server;
public ChatManager(DdonGameServer server)
{
- _server = server;
+ _Server = server;
_handler = new List();
}
@@ -42,7 +43,7 @@ public void SendMessage(string message, string firstName, string lastName, Lobby
response.PhrasesIndex = 0;
foreach (uint characterId in characterIds)
{
- GameClient client = _server.ClientLookup.GetClientByCharacterId(characterId);
+ GameClient client = _Server.ClientLookup.GetClientByCharacterId(characterId);
if (client == null)
{
continue;
@@ -73,6 +74,11 @@ public void SendMessage(string message, string firstName, string lastName, Lobby
response.Recipients.AddRange(recipients);
Send(response);
}
+
+ public void BroadcastMessage(LobbyChatMsgType type, string message)
+ {
+ SendMessage(message, string.Empty, string.Empty, type, _Server.ClientLookup.GetAll());
+ }
public void SendTellMessage(GameClient sender, GameClient receiver, C2SChatSendTellMsgReq request)
{
@@ -89,7 +95,7 @@ public void SendTellMessage(GameClient sender, GameClient receiver, C2SChatSendT
public void SendTellMessageForeign(GameClient client, C2SChatSendTellMsgReq request)
{
- _server.RpcManager.AnnounceTellChat(client, request);
+ _Server.RpcManager.AnnounceTellChat(client, request);
ChatResponse senderChatResponse = new ChatResponse
{
@@ -155,7 +161,7 @@ private void Deliver(GameClient client, ChatResponse response)
{
case LobbyChatMsgType.Say:
case LobbyChatMsgType.Shout:
- response.Recipients.AddRange(_server.ClientLookup.GetAll());
+ response.Recipients.AddRange(_Server.ClientLookup.GetAll());
break;
case LobbyChatMsgType.Party:
PartyGroup party = client.Party;
@@ -171,13 +177,13 @@ private void Deliver(GameClient client, ChatResponse response)
break;
}
- response.Recipients.AddRange(_server.ClientLookup.GetAll().Where(
+ response.Recipients.AddRange(_Server.ClientLookup.GetAll().Where(
x => x.Character != null
&& client.Character != null
&& x.Character.ClanId == client.Character.ClanId)
);
- _server.RpcManager.AnnounceClanChat(client, response);
+ _Server.RpcManager.AnnounceClanChat(client, response);
break;
default:
response.Recipients.Add(client);
diff --git a/Arrowgene.Ddon.GameServer/DdonGameServer.cs b/Arrowgene.Ddon.GameServer/DdonGameServer.cs
index 15d99dacb..a1b4bea1e 100644
--- a/Arrowgene.Ddon.GameServer/DdonGameServer.cs
+++ b/Arrowgene.Ddon.GameServer/DdonGameServer.cs
@@ -79,6 +79,7 @@ public DdonGameServer(GameServerSetting setting, IDatabase database, AssetReposi
ClanManager = new ClanManager(this);
RpcManager = new RpcManager(this);
EpitaphRoadManager = new EpitaphRoadManager(this);
+ ScheduleManager = new ScheduleManager(this);
// Orb Management is slightly complex and requires updating fields across multiple systems
OrbUnlockManager = new OrbUnlockManager(database, WalletManager, JobManager, CharacterManager);
@@ -115,6 +116,7 @@ public DdonGameServer(GameServerSetting setting, IDatabase database, AssetReposi
public ClanManager ClanManager { get; }
public RpcManager RpcManager { get; }
public EpitaphRoadManager EpitaphRoadManager { get; }
+ private ScheduleManager ScheduleManager { get; }
public ChatLogHandler ChatLogHandler { get; }
@@ -129,6 +131,12 @@ public override void Start()
{
QuestManager.LoadQuests(this);
GpCourseManager.EvaluateCourses();
+
+ if (ServerUtils.IsHeadServer(this))
+ {
+ ScheduleManager.StartServerTasks();
+ }
+
LoadChatHandler();
LoadPacketHandler();
base.Start();
diff --git a/Arrowgene.Ddon.GameServer/RpcManager.cs b/Arrowgene.Ddon.GameServer/RpcManager.cs
index 19afd8d7b..0e47f642a 100644
--- a/Arrowgene.Ddon.GameServer/RpcManager.cs
+++ b/Arrowgene.Ddon.GameServer/RpcManager.cs
@@ -9,9 +9,11 @@
using Arrowgene.Logging;
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
+using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
@@ -142,6 +144,11 @@ public List ServerListInfo()
return ChannelInfo.Keys.Select(x => ServerListInfo(x)).ToList();
}
+ public ServerInfo HeadServer()
+ {
+ return ChannelInfo.Values.ToList().OrderBy(x => x.Id).ToList()[0];
+ }
+
public CDataGameServerListInfo ServerListInfo(ushort channelId)
{
var info = ChannelInfo[channelId].ToCDataGameServerListInfo();
@@ -412,5 +419,10 @@ public void AnnounceClanPacket(uint clanId, T packet, uint characterId = 0)
AnnounceClan(clanId, "internal/packet", RpcInternalCommand.AnnouncePacketClan, data);
}
}
+
+ public void AnnounceEpitaphWeeklyReset()
+ {
+ AnnounceAll("internal/packet", RpcInternalCommand.EpitaphRoadWeeklyReset, null);
+ }
}
}
diff --git a/Arrowgene.Ddon.GameServer/ScheduleManager.cs b/Arrowgene.Ddon.GameServer/ScheduleManager.cs
new file mode 100644
index 000000000..24224ea3f
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/ScheduleManager.cs
@@ -0,0 +1,89 @@
+using Arrowgene.Ddon.GameServer.Tasks;
+using Arrowgene.Ddon.Server;
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+using Arrowgene.Logging;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+
+namespace Arrowgene.Ddon.GameServer
+{
+ public class ScheduleManager
+ {
+ private static readonly ServerLogger Logger = LogProvider.Logger(typeof(ScheduleManager));
+
+ private List Tasks;
+ private DdonGameServer Server;
+
+ private static readonly int TIMER_TICK_HOURLY = 1 * 1000; // 1 second
+ private static readonly int TIMER_TICK_DAILY = 10 * 1000; // 10 seconds
+ private static readonly int TIMER_TICK_WEEKLY = 30 * 1000; // 30 seconds
+
+ public ScheduleManager(DdonGameServer server)
+ {
+ Server = server;
+
+ // TODO: Load from server config
+ Tasks = new List()
+ {
+ new EpitaphSchedulerTask(DayOfWeek.Monday, 5, 0)
+ };
+ }
+
+ private int GetTimerTick(ScheduleInterval interval)
+ {
+ switch (interval)
+ {
+ case ScheduleInterval.Hourly:
+ return TIMER_TICK_HOURLY;
+ case ScheduleInterval.Daily:
+ return TIMER_TICK_DAILY;
+ case ScheduleInterval.Weekly:
+ return TIMER_TICK_WEEKLY;
+ default:
+ return TIMER_TICK_HOURLY;
+ }
+ }
+
+ public void StartServerTasks()
+ {
+ Dictionary entries = Server.Database.SelectAllTaskEntries();
+
+ var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ foreach (var task in Tasks)
+ {
+ if (!entries.ContainsKey(task.Type))
+ {
+ Logger.Error($"Task '{task.Type}' has no record in the database. Skipping.");
+ continue;
+ }
+
+ if (!task.IsEnabled(Server))
+ {
+ // This task is not enabled so skip it
+ continue;
+ }
+
+ long nextAction = entries[task.Type].Timestamp;
+ if (now >= nextAction)
+ {
+ task.RunTask(Server);
+ entries[task.Type].Timestamp = task.NextTimestamp();
+ Server.Database.UpdateScheduleInfo(task.Type, entries[task.Type].Timestamp);
+ }
+
+ var timerTick = GetTimerTick(task.Interval);
+ var Timer = new Timer(state =>
+ {
+ long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
+ if (now >= entries[task.Type].Timestamp)
+ {
+ task.RunTask(Server);
+ entries[task.Type].Timestamp = task.NextTimestamp();
+ Server.Database.UpdateScheduleInfo(task.Type, entries[task.Type].Timestamp);
+ }
+ }, null, timerTick, timerTick);
+ }
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/ServerUtils.cs b/Arrowgene.Ddon.GameServer/ServerUtils.cs
new file mode 100644
index 000000000..01cffea96
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/ServerUtils.cs
@@ -0,0 +1,12 @@
+using System.Linq;
+
+namespace Arrowgene.Ddon.GameServer
+{
+ public class ServerUtils
+ {
+ public static bool IsHeadServer(DdonGameServer server)
+ {
+ return server.RpcManager.HeadServer().Id == server.Id;
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/Tasks/DailyTask.cs b/Arrowgene.Ddon.GameServer/Tasks/DailyTask.cs
new file mode 100644
index 000000000..e0a671a37
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/Tasks/DailyTask.cs
@@ -0,0 +1,23 @@
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+using System;
+
+namespace Arrowgene.Ddon.GameServer.Tasks
+{
+ public abstract class DailyTask : SchedulerTask
+ {
+ public uint Hour { get; }
+ public uint Minute { get; }
+
+ public DailyTask(TaskType scheduleType, uint hour, uint minute) : base(ScheduleInterval.Daily, scheduleType)
+ {
+ Hour = hour;
+ Minute = minute;
+ }
+
+ public override long NextTimestamp()
+ {
+ var tomorrow = DateTime.Today.AddDays(1);
+ return new DateTimeOffset(new DateTime(tomorrow.Year, tomorrow.Month, tomorrow.Day, (int)Hour, (int)Minute, 0)).ToUnixTimeSeconds();
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/Tasks/EpitaphSchedulerTask.cs b/Arrowgene.Ddon.GameServer/Tasks/EpitaphSchedulerTask.cs
new file mode 100644
index 000000000..ff835cb9f
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/Tasks/EpitaphSchedulerTask.cs
@@ -0,0 +1,32 @@
+using Arrowgene.Ddon.Server;
+using Arrowgene.Ddon.Shared.Model;
+using Arrowgene.Ddon.Shared.Model.Rpc;
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+using Arrowgene.Logging;
+using System;
+
+namespace Arrowgene.Ddon.GameServer.Tasks
+{
+ public class EpitaphSchedulerTask : WeeklyTask
+ {
+ private static readonly ServerLogger Logger = LogProvider.Logger(typeof(EpitaphSchedulerTask));
+
+ public EpitaphSchedulerTask(DayOfWeek day, uint hour, uint minute) : base(TaskType.EpitaphRoadRewardsReset, day, hour, minute)
+ {
+
+ }
+
+ public override bool IsEnabled(DdonGameServer server)
+ {
+ return server.Setting.GameLogicSetting.EnableEpitaphWeeklyRewards.Value;
+ }
+
+ public override void RunTask(DdonGameServer server)
+ {
+ Logger.Info("Performing weekly epitaph reset");
+ server.Database.DeleteWeeklyEpitaphClaimedRewards();
+
+ server.RpcManager.AnnounceEpitaphWeeklyReset();
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/Tasks/HourlyTask.cs b/Arrowgene.Ddon.GameServer/Tasks/HourlyTask.cs
new file mode 100644
index 000000000..b4360803f
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/Tasks/HourlyTask.cs
@@ -0,0 +1,24 @@
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+using System;
+
+namespace Arrowgene.Ddon.GameServer.Tasks
+{
+ public abstract class HourlyTask : SchedulerTask
+ {
+ ///
+ /// Task which is always scheduled to run on the hour in the timezone of the server currently running.
+ ///
+ /// The task type associated with this task
+ public HourlyTask(TaskType type) : base(ScheduleInterval.Hourly, type)
+ {
+ }
+
+ public override long NextTimestamp()
+ {
+ var now = DateTime.Now;
+ var next = now.AddHours(1);
+ var nextTime = new DateTime(next.Year, next.Month, next.Day, next.Hour, 0, 0);
+ return new DateTimeOffset(now.Add(nextTime - now)).ToUnixTimeSeconds();
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/Tasks/NextTimeAmountTask.cs b/Arrowgene.Ddon.GameServer/Tasks/NextTimeAmountTask.cs
new file mode 100644
index 000000000..ad030fd08
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/Tasks/NextTimeAmountTask.cs
@@ -0,0 +1,22 @@
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+using System;
+
+namespace Arrowgene.Ddon.GameServer.Tasks
+{
+ public abstract class NextTimeAmountTask : SchedulerTask
+ {
+ public uint Hours { get; }
+ public uint Minutes { get; }
+
+ public NextTimeAmountTask(TaskType type, uint hours, uint minutes = 0) : base(ScheduleInterval.Hourly, type)
+ {
+ Hours = hours;
+ Minutes = minutes;
+ }
+
+ public override long NextTimestamp()
+ {
+ return new DateTimeOffset(DateTime.Now.AddHours(Hours).AddMinutes(Minutes)).ToUnixTimeSeconds();
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/Tasks/SchedulerTask.cs b/Arrowgene.Ddon.GameServer/Tasks/SchedulerTask.cs
new file mode 100644
index 000000000..fcd76a083
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/Tasks/SchedulerTask.cs
@@ -0,0 +1,49 @@
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+
+namespace Arrowgene.Ddon.GameServer.Tasks
+{
+ public abstract class SchedulerTask
+ {
+ public TaskType Type { get; }
+ public ScheduleInterval Interval { get; }
+
+ ///
+ /// Constructor for SchedulerTask.
+ ///
+ /// Hint for the type of interval this task is expected to occur at.
+ ///
+ /// The task type which is stored in the DB and used to resume the scheduler
+ /// timer when the head server starts.
+ ///
+ public SchedulerTask(ScheduleInterval interval, TaskType type)
+ {
+ Type = type;
+ Interval = interval;
+ }
+
+ ///
+ /// Runs on the head server. Should deal with things like modifying the database.
+ /// Should use the RPC manage if it is required to update clients on different channels
+ /// or send annoucements to players.
+ ///
+ /// The head server object
+ public abstract void RunTask(DdonGameServer server);
+
+ ///
+ /// Generates the next unix timestamp to store in the database for the task.
+ ///
+ /// Returns the unix timestamp which represents the next time this task should activate.
+ public abstract long NextTimestamp();
+
+ ///
+ /// By default, all tasks will return that they are enabled. A child class can override
+ /// this function to provide custom checks for enablement.
+ ///
+ /// The head server object
+ /// Returns true if this task is enabled, otherwise false.
+ public virtual bool IsEnabled(DdonGameServer server)
+ {
+ return true;
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/Tasks/WeeklyTask.cs b/Arrowgene.Ddon.GameServer/Tasks/WeeklyTask.cs
new file mode 100644
index 000000000..713a863ec
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/Tasks/WeeklyTask.cs
@@ -0,0 +1,37 @@
+using Arrowgene.Ddon.GameServer.Utils;
+using Arrowgene.Ddon.Shared.Model.Scheduler;
+using System;
+
+namespace Arrowgene.Ddon.GameServer.Tasks
+{
+ public abstract class WeeklyTask : SchedulerTask
+ {
+ public DayOfWeek Day { get; }
+ public uint Hour { get; }
+ public uint Minute { get; }
+
+ ///
+ /// Creates a task which runs on a weekly cadence.
+ /// Uses the timezone of the head server when calculating times.
+ ///
+ /// The type of event this is associated with
+ /// The day during the week the reset should occur.
+ /// The hour the reset should occur in a 24 hour format
+ public WeeklyTask(TaskType scheduleType, DayOfWeek day, uint hour, uint minute) : base(ScheduleInterval.Weekly, scheduleType)
+ {
+ Day = day;
+ Hour = hour;
+ Minute = minute;
+ }
+
+ ///
+ /// Calculates the next timestamp for this task in unix seconds
+ ///
+ /// Returns the next timestamp in unix seconds
+ public override long NextTimestamp()
+ {
+ var nextDate = DateUtils.GetNextWeekday(DateTime.Today.AddDays(1), Day);
+ return new DateTimeOffset(new DateTime(nextDate.Year, nextDate.Month, nextDate.Day, (int)Hour, (int)Minute, 0)).ToUnixTimeSeconds();
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.GameServer/Utils/DateUtils.cs b/Arrowgene.Ddon.GameServer/Utils/DateUtils.cs
new file mode 100644
index 000000000..d6bd94908
--- /dev/null
+++ b/Arrowgene.Ddon.GameServer/Utils/DateUtils.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Arrowgene.Ddon.GameServer.Utils
+{
+ public class DateUtils
+ {
+ public static DateTime GetNextWeekday(DateTime start, DayOfWeek day)
+ {
+ // The (... + 7) % 7 ensures we end up with a value in the range [0, 6]
+ int daysToAdd = ((int)day - (int)start.DayOfWeek + 7) % 7;
+ return start.AddDays(daysToAdd);
+ }
+ }
+}
diff --git a/Arrowgene.Ddon.Rpc.Web/Route/Internal/PacketRoute.cs b/Arrowgene.Ddon.Rpc.Web/Route/Internal/PacketRoute.cs
index b6897c934..89d0d44b7 100644
--- a/Arrowgene.Ddon.Rpc.Web/Route/Internal/PacketRoute.cs
+++ b/Arrowgene.Ddon.Rpc.Web/Route/Internal/PacketRoute.cs
@@ -168,6 +168,9 @@ public override RpcCommandResult Execute(DdonGameServer gameServer)
Message = $"AnnouncePacketClan Ch.{_entry.Origin} ClanID {data.ClanId} -> {packet.Id}"
};
}
+ case RpcInternalCommand.EpitaphRoadWeeklyReset:
+ gameServer.EpitaphRoadManager.PerformWeeklyReset();
+ return new RpcCommandResult(this, true);
default:
return new RpcCommandResult(this, false);
}
diff --git a/Arrowgene.Ddon.Server/GameLogicSetting.cs b/Arrowgene.Ddon.Server/GameLogicSetting.cs
index 936aa137e..201e1b75f 100644
--- a/Arrowgene.Ddon.Server/GameLogicSetting.cs
+++ b/Arrowgene.Ddon.Server/GameLogicSetting.cs
@@ -240,7 +240,7 @@ public class GameLogicSetting
///
/// Configures if epitaph rewards are limited once per weekly reset.
///
- [DataMember(Order = 37)] public bool EnableEpitaphWeeklyRewards { get; set; }
+ [DataMember(Order = 37)] public bool? EnableEpitaphWeeklyRewards { get; set; } = true;
/// Enables main pawns in party to gain EXP and JP from quests
/// Original game apparantly did not have pawns share quest reward, so will set to false for default,
@@ -450,6 +450,8 @@ void OnDeserialized(StreamingContext context)
UrlChargeB ??= string.Empty;
UrlCompanionImage ??= string.Empty;
+ EnableEpitaphWeeklyRewards ??= true;
+
EnemyExpModifier ??= 1;
QuestExpModifier ??= 1;
PpModifier ??= 1;
diff --git a/Arrowgene.Ddon.Shared/Model/Rpc/RpcInternalCommand.cs b/Arrowgene.Ddon.Shared/Model/Rpc/RpcInternalCommand.cs
index 4589e334c..b4a8a1359 100644
--- a/Arrowgene.Ddon.Shared/Model/Rpc/RpcInternalCommand.cs
+++ b/Arrowgene.Ddon.Shared/Model/Rpc/RpcInternalCommand.cs
@@ -10,5 +10,7 @@ public enum RpcInternalCommand
AnnouncePacketAll, // RpcPacketData
AnnouncePacketClan, // RpcPacketData
+
+ EpitaphRoadWeeklyReset
}
}
diff --git a/Arrowgene.Ddon.Shared/Model/Scheduler/ScheduleInterval.cs b/Arrowgene.Ddon.Shared/Model/Scheduler/ScheduleInterval.cs
new file mode 100644
index 000000000..e2a1cfefb
--- /dev/null
+++ b/Arrowgene.Ddon.Shared/Model/Scheduler/ScheduleInterval.cs
@@ -0,0 +1,11 @@
+namespace Arrowgene.Ddon.Shared.Model.Scheduler
+{
+ public enum ScheduleInterval : uint
+ {
+ Unknown = 0,
+ Hourly = 1,
+ Daily = 2,
+ Weekly = 3,
+ Monthly = 4
+ }
+}
diff --git a/Arrowgene.Ddon.Shared/Model/Scheduler/SchedulerTaskEntry.cs b/Arrowgene.Ddon.Shared/Model/Scheduler/SchedulerTaskEntry.cs
new file mode 100644
index 000000000..0e5da0cfa
--- /dev/null
+++ b/Arrowgene.Ddon.Shared/Model/Scheduler/SchedulerTaskEntry.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Arrowgene.Ddon.Shared.Model.Scheduler
+{
+ public class SchedulerTaskEntry
+ {
+ public TaskType Type { get; set; }
+ public long Timestamp { get; set; }
+ }
+}
diff --git a/Arrowgene.Ddon.Shared/Model/Scheduler/TaskType.cs b/Arrowgene.Ddon.Shared/Model/Scheduler/TaskType.cs
new file mode 100644
index 000000000..7844968ba
--- /dev/null
+++ b/Arrowgene.Ddon.Shared/Model/Scheduler/TaskType.cs
@@ -0,0 +1,34 @@
+namespace Arrowgene.Ddon.Shared.Model.Scheduler
+{
+ public enum TaskType : uint
+ {
+ // Types derived from http://ddon.wikidot.com/gameplay:home
+ // Possible to add new types other than from this list
+ // Try not to change values as this will confuse the scheduler
+ // in an existing database (unless they are not being used yet)
+ Unknown = 0,
+ RevivalGreenGemstones = 1,
+ LoginStamps = 2,
+ WeakenedStatusRecovery = 3,
+ AreaPointReset = 4,
+ AreaMasterSupportItems = 5,
+ BoardQuestRotation = 6,
+ SpecialBoardQuestRotation = 7,
+ WorldQuestRotation = 8,
+ ClanQuestRotation = 9,
+ SubstoryQuestRotation = 10,
+ ExtremeMissionRewardUpdate = 11,
+ PawnExpedition = 12,
+ PawnAffectionIncreaseInteraction = 13,
+ PawnTrainingExperiencePoints = 14,
+ ClanDungeonReset = 15,
+ MandragoraGrowth = 16,
+ GoldenTreasureChestReset = 17,
+ VioletTreasureChestReset = 18,
+ EpitaphRoadRewardsReset = 19,
+ AwardBitterblackMazeResetTickets = 20,
+ TimeLockedDungeons = 21,
+ // Others not from above webpage
+ SeasonalEventSchedule = 22
+ }
+}
diff --git a/Arrowgene.Ddon.Test/Database/DatabaseMigratorTest.cs b/Arrowgene.Ddon.Test/Database/DatabaseMigratorTest.cs
index ae3d08073..722d59602 100644
--- a/Arrowgene.Ddon.Test/Database/DatabaseMigratorTest.cs
+++ b/Arrowgene.Ddon.Test/Database/DatabaseMigratorTest.cs
@@ -8,6 +8,7 @@
using Arrowgene.Ddon.Shared.Model.BattleContent;
using Arrowgene.Ddon.Shared.Model.Clan;
using Arrowgene.Ddon.Shared.Model.Quest;
+using Arrowgene.Ddon.Shared.Model.Scheduler;
using System;
using System.Collections.Generic;
using System.Data;
@@ -419,6 +420,9 @@ public bool UpdateRentalPawnSlot(uint characterId, uint num)
public bool InsertEpitaphWeeklyReward(uint characterId, uint epitaphId, DbConnection? connectionIn = null) { return true; }
public HashSet GetEpitaphClaimedWeeklyRewards(uint characterId, DbConnection? connectionIn = null) { return new(); }
+ public Dictionary SelectAllTaskEntries() { return new(); }
+ public bool UpdateScheduleInfo(TaskType type, long timestamp) { return true; }
+
public void AddParameter(DbCommand command, string name, object? value, DbType type) { }
public void AddParameter(DbCommand command, string name, string value) { }
public void AddParameter(DbCommand command, string name, Int32 value) { }
@@ -443,7 +447,7 @@ public void AddParameter(DbCommand command, string name, bool value) { }
public byte[] GetBytes(DbDataReader reader, string column, int size) { return null; }
public List SelectRegisteredPawns(Character searchingCharacter, CDataPawnSearchParameter searchParams) { return new List(); }
public List SelectRegisteredPawns(DbConnection conn, Character searchingCharacter, CDataPawnSearchParameter searchParams) { return new List(); }
- public void DeleteWeeklyRewards(DbConnection? connectionIn = null) { }
+ public void DeleteWeeklyEpitaphClaimedRewards(DbConnection? connectionIn = null) { }
}
class MockMigrationStrategy : IMigrationStrategy