tools(ace-mods): add @settime ACE mod for same-clock retail/acdream sky comparisons
Sky and weather gates need retail and acdream looking at the SAME Dereth hour and the SAME authored day group; until now the only lever was acdream's ACDREAM_WORLD_TIME/ACDREAM_DAY_GROUP pins, which retail cannot follow, so the user could never judge night-sky differences (aurora, dome edge) side by side. @settime <hour|fraction> [Sunny|Rainy|Clear|Cloudy|<index>] jumps ACE's Timers.PortalYearTicks FORWARD to the next matching instant. Every TimeSync header carries that value, so both clients re-sync within seconds. Forward-only because DelayActions compare against the same clock (a backward jump would stall them). The calendar and day-group roll duplicate acdream's retail ports (DerethCalendar shift 3600, SkyDayGroupSelector LCG from SkyDesc::CalcPresentDayGroup @0x00500E10) and the 20-entry name table was dumped from the installed Region DAT. Install: copy SetTime.dll + Meta.json to C:\ACE\Mods\SetTime and run @mod find (or restart ACE). Dev servers only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cc42edc8e3
commit
13f685ee94
4 changed files with 238 additions and 0 deletions
10
tools/ace-mods/SetTime/Meta.json
Normal file
10
tools/ace-mods/SetTime/Meta.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"Name": "SetTime",
|
||||
"Author": "acdream",
|
||||
"Description": "@settime <hour|fraction> [Sunny|Rainy|Clear|Cloudy|<dayGroupIndex>] - jump the server's Dereth clock forward so every client (retail and acdream) sees the same hour and day group.",
|
||||
"Version": "1.0",
|
||||
"Priority": 0,
|
||||
"Enabled": true,
|
||||
"HotReload": true,
|
||||
"RegisterCommands": true
|
||||
}
|
||||
175
tools/ace-mods/SetTime/Mod.cs
Normal file
175
tools/ace-mods/SetTime/Mod.cs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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;
|
||||
|
||||
/// <summary>
|
||||
/// ACE mod host entry. Commands are discovered by attribute; nothing is patched.
|
||||
/// </summary>
|
||||
public class Mod : IHarmonyMod
|
||||
{
|
||||
public void Initialize() { }
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <c>@settime</c>: 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 <c>Timers.PortalYearTicks</c> into
|
||||
/// every TimeSync packet header, so retail and acdream both follow within
|
||||
/// seconds.
|
||||
///
|
||||
/// <para>Forward-only on purpose: <c>DelayAction</c>s 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).</para>
|
||||
///
|
||||
/// <para>Calendar + day-group roll are acdream's retail ports
|
||||
/// (<c>DerethDateTime</c>, <c>SkyDayGroupSelector</c>; retail
|
||||
/// <c>SkyDesc::CalcPresentDayGroup @0x00500E10</c>): day = 7620 ticks, year
|
||||
/// = 360 days, Dereth's <c>ZeroTimeOfYear</c> = 3600, seed = absoluteYear *
|
||||
/// 360 + dayOfYear, LCG 0x6A42FDB2 / 0x8ABE1652, index = floor(20 * hash /
|
||||
/// 2^32). Day-group names are Dereth's authored 20-group table.</para>
|
||||
/// </summary>
|
||||
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|<index>).",
|
||||
"<hour|fraction> [Sunny|Rainy|Clear|Cloudy|<dayGroupIndex>]\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<int, bool>? 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));
|
||||
}
|
||||
}
|
||||
31
tools/ace-mods/SetTime/README.md
Normal file
31
tools/ace-mods/SetTime/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# SetTime — ACE mod for side-by-side sky/weather comparisons
|
||||
|
||||
`@settime <hour|fraction> [Sunny|Rainy|Clear|Cloudy|<dayGroupIndex>]`
|
||||
|
||||
Jumps the server's Dereth clock **forward** (never back) so retail and
|
||||
acdream — both driven by ACE's TimeSync — show the same hour and the same
|
||||
authored day group:
|
||||
|
||||
- `@settime 3` → the next 3 o'clock (day fraction 0.125);
|
||||
- `@settime 0.125 Rainy` → 3 o'clock on the next day whose roll is a Rainy group;
|
||||
- `@settime 12 16` → noon on the next day that rolls day group 16.
|
||||
|
||||
Dev servers only: a forward jump fires every pending `DelayAction` early.
|
||||
The command needs `AccessLevel.Admin`.
|
||||
|
||||
## Build and install
|
||||
|
||||
```powershell
|
||||
dotnet build tools/ace-mods/SetTime -c Release
|
||||
New-Item -ItemType Directory -Force C:\ACE\Mods\SetTime | Out-Null
|
||||
Copy-Item tools/ace-mods/SetTime/bin/Release/net10.0/SetTime.dll, tools/ace-mods/SetTime/Meta.json C:\ACE\Mods\SetTime\
|
||||
```
|
||||
|
||||
Then restart ACE (the mod loader also hot-reloads when `HotReload` is true).
|
||||
`AceServerDir` (default `C:\ACE\Server`) can be overridden with
|
||||
`-p:AceServerDir=...` if the server lives elsewhere.
|
||||
|
||||
The calendar and day-group roll are the same retail ports acdream uses
|
||||
(`DerethDateTime`, `SkyDayGroupSelector`; retail
|
||||
`SkyDesc::CalcPresentDayGroup @0x00500E10`), so the group the command
|
||||
predicts is the group both clients will render.
|
||||
22
tools/ace-mods/SetTime/SetTime.csproj
Normal file
22
tools/ace-mods/SetTime/SetTime.csproj
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- ACE server mod: @settime. Build: dotnet build -c Release; install: copy
|
||||
bin/Release/net10.0/SetTime.dll + Meta.json to C:\ACE\Mods\SetTime\ and
|
||||
restart ACE. See README.md. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AssemblyName>SetTime</AssemblyName>
|
||||
<RootNamespace>SetTime</RootNamespace>
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
<RestorePackagesWithLockFile>false</RestorePackagesWithLockFile>
|
||||
<RestoreLockedMode>false</RestoreLockedMode>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<AceServerDir Condition="'$(AceServerDir)' == ''">C:\ACE\Server</AceServerDir>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="ACE.Server"><HintPath>$(AceServerDir)\ACE.Server.dll</HintPath><Private>false</Private></Reference>
|
||||
<Reference Include="ACE.Entity"><HintPath>$(AceServerDir)\ACE.Entity.dll</HintPath><Private>false</Private></Reference>
|
||||
<Reference Include="ACE.Common"><HintPath>$(AceServerDir)\ACE.Common.dll</HintPath><Private>false</Private></Reference>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Loading…
Add table
Add a link
Reference in a new issue