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));
}
}