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