#W# Merge: Merged with upstream. Conflicts resolved.
This commit is contained in:
commit
f6f50470a9
276 changed files with 6742 additions and 8845 deletions
|
|
@ -52,6 +52,12 @@ public static class MultiData
|
|||
public static MultiComponentList GetComponents(int multiID) =>
|
||||
_components.TryGetValue(multiID & 0x3FFF, out var mcl) ? mcl : MultiComponentList.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a footprint without multi files, for hosts that generate their own (tests).
|
||||
/// </summary>
|
||||
internal static void SetComponents(int multiID, MultiComponentList components) =>
|
||||
_components[multiID & 0x3FFF] = components;
|
||||
|
||||
private static void LoadUOP(string path)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
|
|
|
|||
|
|
@ -1042,13 +1042,14 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
|
||||
writer.Write((int)flags);
|
||||
|
||||
// Anchored: shifted by downtime at load, so time-since-moved is preserved and the
|
||||
// bytes are stable across saves while the item does not move.
|
||||
writer.WriteAnchoredTime(LastMoved);
|
||||
// Absolute and never shifted at load, so an item that does not move serializes to the
|
||||
// same bytes across saves and across restarts. Downtime credit for a decay countdown that
|
||||
// was running at save time goes into DecayResetTime (CreditDowntimeToDecay).
|
||||
writer.Write(LastMoved);
|
||||
|
||||
if (GetSaveFlag(flags, SaveFlag.DecayReset))
|
||||
{
|
||||
writer.WriteAnchoredTime(info.m_DecayReset);
|
||||
writer.Write(info.m_DecayReset);
|
||||
}
|
||||
|
||||
if (GetSaveFlag(flags, SaveFlag.Direction))
|
||||
|
|
@ -2494,6 +2495,30 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
UpdateDecayRegistration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Credits the downtime before this load to a decay countdown that was still running when the
|
||||
/// save started, so the shard being down does not age the item. <see cref="LastMoved" /> stays
|
||||
/// the real time of the move; the credit goes into <see cref="DecayResetTime" />, which only
|
||||
/// the items moved within their <see cref="DecayTime" /> of the save ever carry. Called once
|
||||
/// per item after the world loads, when <see cref="DecayTime" /> is final. An item already
|
||||
/// due at save time gets nothing: it is due either way.
|
||||
/// </summary>
|
||||
internal void CreditDowntimeToDecay(DateTime saveStartUtc, TimeSpan downtime)
|
||||
{
|
||||
if (downtime <= TimeSpan.Zero || !Decays)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var reset = DecayResetTime;
|
||||
var start = reset > LastMoved ? reset : LastMoved;
|
||||
|
||||
if (start + DecayTime > saveStartUtc)
|
||||
{
|
||||
DecayResetTime = start + downtime;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateDecayRegistration()
|
||||
{
|
||||
DecayScheduler.Unregister(this);
|
||||
|
|
@ -2856,7 +2881,9 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
|
||||
if (version >= 11)
|
||||
{
|
||||
LastMoved = reader.ReadAnchoredTime();
|
||||
// Written with WriteAnchoredTime before LastMoved stopped being shifted;
|
||||
// the bytes are the same absolute ticks either way.
|
||||
LastMoved = reader.ReadDateTime();
|
||||
}
|
||||
else if (version < 7)
|
||||
{
|
||||
|
|
@ -2878,7 +2905,7 @@ public partial class Item : IHued, IComparable<Item>, ISpawnable, IObjectPropert
|
|||
|
||||
if (version >= 10 && GetSaveFlag(flags, SaveFlag.DecayReset))
|
||||
{
|
||||
var reset = version >= 11 ? reader.ReadAnchoredTime() : reader.ReadDeltaTime();
|
||||
var reset = version >= 11 ? reader.ReadDateTime() : reader.ReadDeltaTime();
|
||||
|
||||
// Pre-v11 LastMoved was stored at whole-minute precision; keep the
|
||||
// stamp only while it still extends the deadline.
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ public static class Localization
|
|||
return false;
|
||||
}
|
||||
|
||||
if (entries.Count == 0)
|
||||
// The fallback table stays registered even when empty: _fallbackEntries points at it, and a
|
||||
// replacement created by a later Add would be invisible to fallback lookups.
|
||||
if (entries.Count == 0 && entries != _fallbackEntries)
|
||||
{
|
||||
_localizations.Remove(lang);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ public static class Core
|
|||
private static volatile bool _performSnapshot;
|
||||
private static string _snapshotPath;
|
||||
|
||||
// 0 = relaunch in-process. Otherwise a restart exits with this code and leaves the relaunch to a
|
||||
// supervisor; a spawned child loses the console and dies with a systemd unit or container.
|
||||
private static int _restartExitCode;
|
||||
|
||||
// A backstop, not a latency control: the wheel's tick rate already bounds the sleep.
|
||||
// Measured across 1/2/4/8ms; 2 is optimal.
|
||||
private static int _eventLoopIdleWaitMs = 2;
|
||||
|
|
@ -434,7 +438,7 @@ public static class Core
|
|||
ConsoleInputHandler.ReadLine();
|
||||
}
|
||||
|
||||
DoKill();
|
||||
DoKill(_restartOnKill);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -468,27 +472,28 @@ public static class Core
|
|||
|
||||
HandleClosed();
|
||||
|
||||
if (restart && _restartExitCode != 0)
|
||||
{
|
||||
logger.Information("Restarting (exiting with code {ExitCode} for the supervisor)", _restartExitCode);
|
||||
Environment.Exit(_restartExitCode);
|
||||
}
|
||||
|
||||
if (restart)
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.Information("Restarting");
|
||||
if (IsWindows)
|
||||
{
|
||||
using var process = Process.Start("dotnet", $"{ApplicationAssembly.Location}");
|
||||
}
|
||||
else
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = $"{ApplicationAssembly.Location}",
|
||||
UseShellExecute = true
|
||||
};
|
||||
|
||||
process.Start();
|
||||
// Relaunch through whichever host started us: the apphost (ModernUO.exe) runs directly,
|
||||
// `dotnet ModernUO.dll` needs the assembly path. ArgumentList quotes paths with spaces.
|
||||
var hostPath = Environment.ProcessPath ?? "dotnet";
|
||||
var startInfo = new ProcessStartInfo(hostPath) { UseShellExecute = false };
|
||||
if (Path.GetFileNameWithoutExtension(hostPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
startInfo.ArgumentList.Add(ApplicationAssembly.Location);
|
||||
}
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
logger.Information("Restart done");
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -630,6 +635,19 @@ public static class Core
|
|||
|
||||
_lateWakePercent = Math.Clamp(lateWakePercent, 0, 100);
|
||||
|
||||
// Unix truncates exit codes to a byte, so anything outside 1-255 cannot be told apart reliably.
|
||||
var restartExitCode = ServerConfiguration.GetSetting("server.restartExitCode", 0);
|
||||
if (restartExitCode is < 0 or > 255)
|
||||
{
|
||||
logger.Warning(
|
||||
"server.restartExitCode {Value} is outside 1-255; restarts will relaunch the server directly",
|
||||
restartExitCode
|
||||
);
|
||||
restartExitCode = 0;
|
||||
}
|
||||
|
||||
_restartExitCode = restartExitCode;
|
||||
|
||||
var assemblyPath = Path.Join(BaseDirectory, AssembliesConfiguration);
|
||||
|
||||
// Load UOContent.dll
|
||||
|
|
|
|||
|
|
@ -2349,9 +2349,11 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
{
|
||||
writer.Write(38); // version
|
||||
|
||||
writer.WriteAnchoredTime(LastStrGain);
|
||||
writer.WriteAnchoredTime(LastIntGain);
|
||||
writer.WriteAnchoredTime(LastDexGain);
|
||||
// Absolute and never shifted at load: the stat-gain cooldown is minutes long, so letting
|
||||
// downtime count toward it costs nothing, and an idle mobile keeps its bytes across restarts.
|
||||
writer.Write(LastStrGain);
|
||||
writer.Write(LastIntGain);
|
||||
writer.Write(LastDexGain);
|
||||
|
||||
byte hairflag = 0x00;
|
||||
|
||||
|
|
@ -6185,9 +6187,9 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
{
|
||||
if (version >= 38)
|
||||
{
|
||||
LastStrGain = reader.ReadAnchoredTime();
|
||||
LastIntGain = reader.ReadAnchoredTime();
|
||||
LastDexGain = reader.ReadAnchoredTime();
|
||||
LastStrGain = reader.ReadDateTime();
|
||||
LastIntGain = reader.ReadDateTime();
|
||||
LastDexGain = reader.ReadDateTime();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -80,6 +80,12 @@ public sealed unsafe class BinaryFileReader : IDisposable, IGenericReader
|
|||
set => _reader.AnchoredTimeShift = value;
|
||||
}
|
||||
|
||||
public DateTime AnchoredTimeOrigin
|
||||
{
|
||||
get => _reader.AnchoredTimeOrigin;
|
||||
set => _reader.AnchoredTimeOrigin = value;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_accessor?.SafeMemoryMappedViewHandle.ReleasePointer();
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ public class BufferReader : IGenericReader
|
|||
|
||||
public TimeSpan AnchoredTimeShift { get; set; }
|
||||
|
||||
public DateTime AnchoredTimeOrigin { get; set; }
|
||||
|
||||
public BufferReader(byte[] buffer, Dictionary<ulong, string> typesDb = null, Encoding encoding = null)
|
||||
{
|
||||
_buffer = buffer;
|
||||
|
|
|
|||
|
|
@ -409,9 +409,9 @@ public class BufferWriter : IGenericWriter
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the absolute value; <see cref="IGenericReader.ReadAnchoredTime" /> re-bases it
|
||||
/// by the elapsed time since the save started, so downtime does not age it and an
|
||||
/// unchanged value serializes to identical bytes.
|
||||
/// Writes the absolute value; <see cref="IGenericReader.ReadAnchoredTime" /> re-bases it by
|
||||
/// the downtime when it was still in the future at save start, so downtime does not consume a
|
||||
/// running deadline and an unchanged value serializes to identical bytes.
|
||||
/// </summary>
|
||||
public void WriteAnchoredTime(DateTime value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -529,6 +529,7 @@ public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPer
|
|||
// The whole save shares one anchor. Publish it so payloads without their own
|
||||
// (GenericPersistence bins) can shift too; indexes load before any of them.
|
||||
World.LoadTimeShift = _anchoredTimeShift;
|
||||
World.LoadedSaveStartTime = anchor;
|
||||
}
|
||||
|
||||
if (version >= 4)
|
||||
|
|
@ -709,7 +710,8 @@ public class GenericEntityPersistence<T> : GenericPersistence, IGenericEntityPer
|
|||
accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr);
|
||||
var dataReader = new UnmanagedDataReader(ptr, accessor.Length, typesDb)
|
||||
{
|
||||
AnchoredTimeShift = _anchoredTimeShift
|
||||
AnchoredTimeShift = _anchoredTimeShift,
|
||||
AnchoredTimeOrigin = World.LoadedSaveStartTime
|
||||
};
|
||||
|
||||
Deserialize(dataReader);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,8 @@ public abstract class GenericPersistence : Persistence, IGenericSerializable
|
|||
// These payloads carry no anchor of their own; they inherit the save-wide
|
||||
// shift stamped while the entity indexes were read (indexes always load
|
||||
// before persistence payloads — see Persistence.Load).
|
||||
AnchoredTimeShift = World.LoadTimeShift
|
||||
AnchoredTimeShift = World.LoadTimeShift,
|
||||
AnchoredTimeOrigin = World.LoadedSaveStartTime
|
||||
};
|
||||
Deserialize(dataReader);
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,19 @@ public interface IGenericReader
|
|||
/// </summary>
|
||||
TimeSpan AnchoredTimeShift => TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// UTC start of the loaded save. <see cref="ReadAnchoredTime" /> leaves a value at or before
|
||||
/// it unshifted. Default when unknown, which shifts every value.
|
||||
/// </summary>
|
||||
DateTime AnchoredTimeOrigin => default;
|
||||
|
||||
/// <summary>
|
||||
/// Reads an anchored timestamp. A deadline still in the future when the save started is
|
||||
/// shifted by <see cref="AnchoredTimeShift" />, so it keeps the time it had left. A value at or
|
||||
/// before the save start is returned as saved: an expired deadline stays expired instead of
|
||||
/// coming back, and a record of something that already happened (a last-use or last-loss stamp)
|
||||
/// keeps its bytes across restarts, with downtime counting toward its window.
|
||||
/// </summary>
|
||||
DateTime ReadAnchoredTime()
|
||||
{
|
||||
var value = ReadDateTime();
|
||||
|
|
@ -75,7 +88,7 @@ public interface IGenericReader
|
|||
}
|
||||
|
||||
var shift = AnchoredTimeShift;
|
||||
if (shift == TimeSpan.Zero)
|
||||
if (shift == TimeSpan.Zero || value <= AnchoredTimeOrigin)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ public unsafe class UnmanagedDataReader : IGenericReader
|
|||
|
||||
public TimeSpan AnchoredTimeShift { get; set; }
|
||||
|
||||
public DateTime AnchoredTimeOrigin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Read bits of data raw from a serialized file using Little-endian.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@
|
|||
<PackageReference Include="IORingGroup" Version="1.0.13" />
|
||||
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.2" />
|
||||
<PackageReference Include="LibDeflate.Bindings" Version="1.0.4" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.12" />
|
||||
<PackageReference Include="System.IO.Hashing" Version="10.0.13" />
|
||||
|
||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="4.2.0" />
|
||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.2.0" PrivateAssets="all" />
|
||||
<PackageReference Include="ModernUO.Serialization.Annotations" Version="4.3.0" />
|
||||
<PackageReference Include="ModernUO.Serialization.Generator" Version="4.3.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Migrations/*.v*.json" />
|
||||
|
|
|
|||
|
|
@ -220,11 +220,17 @@ public class TileMatrix
|
|||
|
||||
public StaticTile[][][] GetStaticBlock(int x, int y)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight || DataStream == null || IndexStream == null)
|
||||
if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight)
|
||||
{
|
||||
return _emptyStaticBlock;
|
||||
}
|
||||
|
||||
// A map without static files (tests, generated maps) serves only what was injected.
|
||||
if (DataStream == null || IndexStream == null)
|
||||
{
|
||||
return _staticTiles[x]?[y] ?? _emptyStaticBlock;
|
||||
}
|
||||
|
||||
_staticTiles[x] ??= new StaticTile[BlockHeight][][][];
|
||||
|
||||
var tiles = _staticTiles[x][y];
|
||||
|
|
@ -292,11 +298,17 @@ public class TileMatrix
|
|||
|
||||
public LandTile[] GetLandBlock(int x, int y)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight || MapStream == null)
|
||||
if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight)
|
||||
{
|
||||
return _invalidLandBlock;
|
||||
}
|
||||
|
||||
// A map without a map file (tests, generated maps) serves only what was injected.
|
||||
if (MapStream == null)
|
||||
{
|
||||
return _landTiles[x]?[y] ?? _invalidLandBlock;
|
||||
}
|
||||
|
||||
_landTiles[x] ??= new LandTile[BlockHeight][];
|
||||
|
||||
var tiles = _landTiles[x][y];
|
||||
|
|
|
|||
|
|
@ -321,13 +321,14 @@ public partial class Timer
|
|||
for (var i = 0; i < _ringSize; i++)
|
||||
{
|
||||
var node = t[i];
|
||||
Timer next;
|
||||
|
||||
do
|
||||
while (node != null)
|
||||
{
|
||||
next = node?._nextTimer;
|
||||
node?.Stop();
|
||||
} while (next != null);
|
||||
// Stop() unlinks the node, so read its successor first.
|
||||
var next = node._nextTimer;
|
||||
node.Stop();
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,12 @@ public static class World
|
|||
/// payloads, which carry no anchor of their own. Zero for saves that predate the anchor.
|
||||
/// </summary>
|
||||
public static TimeSpan LoadTimeShift { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// UTC start time of the save being loaded, read from its index anchor. Default for saves
|
||||
/// that predate the anchor.
|
||||
/// </summary>
|
||||
public static DateTime LoadedSaveStartTime { get; internal set; }
|
||||
public static bool Running => WorldState is not WorldState.Loading and not WorldState.Initial;
|
||||
public static bool Loading => WorldState == WorldState.Loading;
|
||||
|
||||
|
|
@ -674,6 +680,7 @@ public static class World
|
|||
}
|
||||
|
||||
item.ClearProperties();
|
||||
item.CreditDowntimeToDecay(LoadedSaveStartTime, LoadTimeShift);
|
||||
item.UpdateDecayRegistration();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue