using System.Globalization; using System.Reflection; using ACE.Entity.Enum; using ACE.Server.Command; using ACE.Server.Entity; using ACE.Server.Mods; using ACE.Server.Network; using ACE.Server.Network.GameMessages.Messages; namespace SetTime; /// /// ACE mod host entry. Commands are discovered by attribute; nothing is patched. /// public class Mod : IHarmonyMod { public void Initialize() { } public void Dispose() { } } /// /// @settime: jump the server's Dereth clock FORWARD to a day fraction /// (or hour), optionally to the next day whose authored day group rolls to /// the requested weather. ACE writes Timers.PortalYearTicks into /// every TimeSync packet header, so retail and acdream both follow within /// seconds. /// /// Forward-only on purpose: DelayActions compare against /// PortalYearTicks, so a backward jump would freeze every pending server /// action until the clock caught up; a forward jump only fires them early /// (acceptable on a dev server, never on a live shard). /// /// Calendar + day-group roll are acdream's retail ports /// (DerethDateTime, SkyDayGroupSelector; retail /// SkyDesc::CalcPresentDayGroup @0x00500E10): day = 7620 ticks, year /// = 360 days, Dereth's ZeroTimeOfYear = 3600, seed = absoluteYear * /// 360 + dayOfYear, LCG 0x6A42FDB2 / 0x8ABE1652, index = floor(20 * hash / /// 2^32). Day-group names are Dereth's authored 20-group table. /// public static class SetTimeCommands { private const double DayTicks = 7620.0; private const double YearTicks = DayTicks * 360.0; private const double ZeroTimeOfYear = 3600.0; private const int ZeroYear = 10; private const int DayGroupCount = 20; private static readonly string[] DayGroupNames = [ "Sunny", "Sunny", "Sunny", "Rainy", "Sunny", "Sunny", "Sunny", "Rainy", "Clear", "Rainy", "Clear", "Clear", "Cloudy", "Cloudy", "Cloudy", "Rainy", "Rainy", "Rainy", "Rainy", "Rainy", ]; [CommandHandler("settime", AccessLevel.Admin, CommandHandlerFlag.None, 1, "Jumps the server's Dereth clock forward to an hour (0-23) or day fraction (0..1), optionally to the next day with the given day group (Sunny|Rainy|Clear|Cloudy|).", " [Sunny|Rainy|Clear|Cloudy|]\nExamples: @settime 3 | @settime 0.125 Rainy | @settime 12 16")] public static void HandleSetTime(Session session, params string[] parameters) { if (!TryParseFraction(parameters[0], out double fraction)) { Write(session, "settime: first argument must be an hour 0-23 or a day fraction in [0,1) written with a decimal point."); return; } Func? groupMatches = null; string groupLabel = "any"; if (parameters.Length > 1) { string want = parameters[1]; if (int.TryParse(want, NumberStyles.Integer, CultureInfo.InvariantCulture, out int wantIndex)) { if (wantIndex < 0 || wantIndex >= DayGroupCount) { Write(session, $"settime: day group index must be 0..{DayGroupCount - 1}."); return; } groupMatches = g => g == wantIndex; groupLabel = $"{wantIndex} ({DayGroupNames[wantIndex]})"; } else { if (!DayGroupNames.Any(n => string.Equals(n, want, StringComparison.OrdinalIgnoreCase))) { Write(session, "settime: day group must be Sunny, Rainy, Clear, Cloudy or an index 0..19."); return; } groupMatches = g => string.Equals(DayGroupNames[g], want, StringComparison.OrdinalIgnoreCase); groupLabel = want; } } double now = Timers.PortalYearTicks; long dayIndex = (long)Math.Floor((now + ZeroTimeOfYear) / DayTicks); double target = double.NaN; int targetGroup = -1; for (int d = 0; d <= 400; d++) { double candidate = (dayIndex + d) * DayTicks + fraction * DayTicks - ZeroTimeOfYear; if (candidate <= now) continue; int g = DayGroupIndex(candidate); if (groupMatches is null || groupMatches(g)) { target = candidate; targetGroup = g; break; } } if (double.IsNaN(target)) { Write(session, $"settime: no day within 400 days rolls day group {groupLabel}."); return; } PropertyInfo ticks = typeof(Timers).GetProperty( nameof(Timers.PortalYearTicks), BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("Timers.PortalYearTicks not found."); ticks.SetValue(null, target); double jumpedDays = (target - now) / DayTicks; Write(session, $"settime: clock moved forward {jumpedDays:F2} Dereth days to day fraction {fraction:F4} " + $"(hour {fraction * 24:F1}); day group {targetGroup} ({DayGroupNames[targetGroup]}). " + $"PortalYearTicks {now:F0} -> {target:F0}. Clients re-sync on the next TimeSync."); } private static bool TryParseFraction(string text, out double fraction) { fraction = 0; if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) return false; if (text.Contains('.')) { if (value < 0 || value >= 1) return false; fraction = value; return true; } if (value >= 0 && value < 24) { fraction = value / 24.0; return true; } return false; } private static int DayGroupIndex(double ticksValue) { double shifted = ticksValue + ZeroTimeOfYear; int year = (int)(shifted / YearTicks); double tYear = shifted - year * YearTicks; int dayOfYear = Math.Clamp((int)(tYear / DayTicks), 0, 359); int absoluteYear = year + ZeroYear; int seed = unchecked(absoluteYear * 360 + dayOfYear); int mixed = unchecked(seed * 0x6A42FDB2 + unchecked((int)0x8ABE1652)); float hash = mixed; if (mixed < 0) hash += 4294967296.0f; const float inverseTwoTo32 = 1.0f / 4294967296.0f; int index = (int)MathF.Floor(DayGroupCount * hash * inverseTwoTo32); return index < 0 || index >= DayGroupCount ? 0 : index; } private static void Write(Session? session, string message) { if (session is null) { Console.WriteLine(message); // console invocation return; } session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Broadcast)); } }