mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
Add YAML hot reloading (#1571)
* Implement hot reloading for entity prototypes * Implement automatic prototype hot-reloading * Merge fixes * Add yaml hot reloading and a message to notify the client * Add reloading only changed files, remove cooldown, add retries and remove IPrototype * Remove reload command * Make the client listen for reloads instead and only when focused * Fix errors * Only queue a reload when the queue has items in it * Make fails after 10 retries log instead of throw if reloading Co-authored-by: Jackson Lewis <inquisitivepenguin@protonmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ using Robust.Client.Input;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Prototypes;
|
||||
using Robust.Client.Reflection;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
@@ -34,7 +35,7 @@ namespace Robust.Client
|
||||
{
|
||||
SharedIoC.RegisterIoC();
|
||||
|
||||
IoCManager.Register<IPrototypeManager, PrototypeManager>();
|
||||
IoCManager.Register<IPrototypeManager, ClientPrototypeManager>();
|
||||
IoCManager.Register<IEntityManager, ClientEntityManager>();
|
||||
IoCManager.Register<IComponentFactory, ClientComponentFactory>();
|
||||
IoCManager.Register<ITileDefinitionManager, ClydeTileDefinitionManager>();
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace Robust.Client
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_consoleHost.Initialize();
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes/"));
|
||||
_prototypeManager.Resync();
|
||||
_mapManager.Initialize();
|
||||
|
||||
@@ -12,7 +12,7 @@ using YamlDotNet.RepresentationModel;
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
[Prototype("shader")]
|
||||
public sealed class ShaderPrototype : IPrototype, IIndexedPrototype
|
||||
public sealed class ShaderPrototype : IPrototype
|
||||
{
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
119
Robust.Client/Prototypes/ClientPrototypeManager.cs
Normal file
119
Robust.Client/Prototypes/ClientPrototypeManager.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
namespace Robust.Client.Prototypes
|
||||
{
|
||||
public sealed class ClientPrototypeManager : PrototypeManager
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
private readonly List<FileSystemWatcher> _watchers = new();
|
||||
private readonly TimeSpan _reloadDelay = TimeSpan.FromMilliseconds(10);
|
||||
private CancellationTokenSource _reloadToken = new();
|
||||
private readonly HashSet<ResourcePath> _reloadQueue = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
NetManager.RegisterNetMessage<MsgReloadPrototypes>(MsgReloadPrototypes.NAME, accept: NetMessageAccept.Server);
|
||||
|
||||
_clyde.OnWindowFocused += WindowFocusedChanged;
|
||||
|
||||
WatchResources();
|
||||
}
|
||||
|
||||
private void WindowFocusedChanged(WindowFocusedEventArgs args)
|
||||
{
|
||||
#if !FULL_RELEASE
|
||||
if (args.Focused && _reloadQueue.Count > 0)
|
||||
{
|
||||
Timer.Spawn(_reloadDelay, ReloadPrototypeQueue, _reloadToken.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_reloadToken.Cancel();
|
||||
_reloadToken = new CancellationTokenSource();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ReloadPrototypeQueue()
|
||||
{
|
||||
var then = DateTime.Now;
|
||||
|
||||
var msg = NetManager.CreateNetMessage<MsgReloadPrototypes>();
|
||||
msg.Paths = _reloadQueue.ToArray();
|
||||
NetManager.ClientSendMessage(msg);
|
||||
|
||||
foreach (var path in _reloadQueue)
|
||||
{
|
||||
ReloadPrototypes(path);
|
||||
}
|
||||
|
||||
Logger.Info($"Reloaded prototypes in {(int) (DateTime.Now - then).TotalMilliseconds} ms");
|
||||
}
|
||||
|
||||
private void WatchResources()
|
||||
{
|
||||
#if !FULL_RELEASE
|
||||
foreach (var path in Resources.GetContentRoots().Select(r => r.ToString())
|
||||
.Where(r => Directory.Exists(r + "/Prototypes")).Select(p => p + "/Prototypes"))
|
||||
{
|
||||
var watcher = new FileSystemWatcher(path, "*.yml")
|
||||
{
|
||||
IncludeSubdirectories = true,
|
||||
NotifyFilter = NotifyFilters.LastWrite
|
||||
};
|
||||
|
||||
watcher.Changed += (_, args) =>
|
||||
{
|
||||
switch (args.ChangeType)
|
||||
{
|
||||
case WatcherChangeTypes.Renamed:
|
||||
case WatcherChangeTypes.Deleted:
|
||||
return;
|
||||
case WatcherChangeTypes.Created:
|
||||
// case WatcherChangeTypes.Deleted:
|
||||
case WatcherChangeTypes.Changed:
|
||||
case WatcherChangeTypes.All:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
TaskManager.RunOnMainThread(() =>
|
||||
{
|
||||
var file = new ResourcePath(args.FullPath);
|
||||
|
||||
foreach (var root in IoCManager.Resolve<IResourceManager>().GetContentRoots())
|
||||
{
|
||||
if (!file.TryRelativeTo(root, out var relative))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_reloadQueue.Add(relative);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watcher.EnableRaisingEvents = true;
|
||||
_watchers.Add(watcher);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,8 +272,13 @@ namespace Robust.Server
|
||||
|
||||
//IoCManager.Resolve<IMapLoader>().LoadedMapData +=
|
||||
// IoCManager.Resolve<IRobustMappedStringSerializer>().AddStrings;
|
||||
IoCManager.Resolve<IPrototypeManager>().LoadedData +=
|
||||
(yaml, name) => _stringSerializer.AddStrings(yaml);
|
||||
IoCManager.Resolve<IPrototypeManager>().LoadedData += (yaml, name) =>
|
||||
{
|
||||
if (!_stringSerializer.Locked)
|
||||
{
|
||||
_stringSerializer.AddStrings(yaml);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize Tier 2 services
|
||||
IoCManager.Resolve<IGameTiming>().InSimulation = true;
|
||||
@@ -296,6 +301,7 @@ namespace Robust.Server
|
||||
// because of 'reasons' this has to be called after the last assembly is loaded
|
||||
// otherwise the prototypes will be cleared
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
prototypeManager.Initialize();
|
||||
prototypeManager.LoadDirectory(new ResourcePath(@"/Prototypes"));
|
||||
prototypeManager.Resync();
|
||||
|
||||
|
||||
@@ -30,5 +30,10 @@ namespace Robust.Server.Console
|
||||
{
|
||||
return Implementation?.CanAdminMenu(session) ?? false;
|
||||
}
|
||||
|
||||
public bool CanAdminReloadPrototypes(IPlayerSession session)
|
||||
{
|
||||
return Implementation?.CanAdminReloadPrototypes(session) ?? false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ namespace Robust.Server.Console
|
||||
bool CanAdminPlace(IPlayerSession session);
|
||||
bool CanScript(IPlayerSession session);
|
||||
bool CanAdminMenu(IPlayerSession session);
|
||||
bool CanAdminReloadPrototypes(IPlayerSession session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,50 @@
|
||||
using System;
|
||||
using Robust.Server.Console;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Server.Prototypes
|
||||
{
|
||||
public sealed class ServerPrototypeManager : PrototypeManager
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IConGroupController _conGroups = default!;
|
||||
|
||||
public ServerPrototypeManager() : base()
|
||||
{
|
||||
RegisterIgnore("shader");
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
NetManager.RegisterNetMessage<MsgReloadPrototypes>(MsgReloadPrototypes.NAME, HandleReloadPrototypes, NetMessageAccept.Server);
|
||||
}
|
||||
|
||||
private void HandleReloadPrototypes(MsgReloadPrototypes msg)
|
||||
{
|
||||
#if !FULL_RELEASE
|
||||
if (!_playerManager.TryGetSessionByChannel(msg.MsgChannel, out var player) ||
|
||||
!_conGroups.CanAdminReloadPrototypes(player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var then = DateTime.Now;
|
||||
|
||||
foreach (var path in msg.Paths)
|
||||
{
|
||||
ReloadPrototypes(path);
|
||||
}
|
||||
|
||||
Logger.Info($"Reloaded prototypes in {(int) (DateTime.Now - then).TotalMilliseconds} ms");
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,12 @@ namespace Robust.Shared.ContentPack
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
|
||||
IEnumerable<ResourcePath> ContentFindFiles(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of paths to all top-level content directories
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<ResourcePath> GetContentRoots();
|
||||
|
||||
/// <summary>
|
||||
/// Read a file from the mounted content paths to a string.
|
||||
/// </summary>
|
||||
@@ -168,4 +174,4 @@ namespace Robust.Shared.ContentPack
|
||||
return new StreamReader(stream, encoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +305,17 @@ namespace Robust.Shared.ContentPack
|
||||
AddRoot(ResourcePath.Root, loader);
|
||||
}
|
||||
|
||||
public IEnumerable<ResourcePath> GetContentRoots()
|
||||
{
|
||||
foreach (var (_, root) in _contentRoots)
|
||||
{
|
||||
if (root is DirLoader loader)
|
||||
{
|
||||
yield return new ResourcePath(loader.GetPath(new ResourcePath(@"/")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool IsPathValid(ResourcePath path)
|
||||
{
|
||||
var asString = path.ToString();
|
||||
|
||||
42
Robust.Shared/Network/Messages/MsgReloadPrototypes.cs
Normal file
42
Robust.Shared/Network/Messages/MsgReloadPrototypes.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Lidgren.Network;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Network.Messages
|
||||
{
|
||||
public class MsgReloadPrototypes : NetMessage
|
||||
{
|
||||
#region REQUIRED
|
||||
|
||||
public const MsgGroups GROUP = MsgGroups.Command;
|
||||
public const string NAME = nameof(MsgReloadPrototypes);
|
||||
|
||||
public MsgReloadPrototypes(INetChannel channel) : base(NAME, GROUP)
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ResourcePath[] Paths = default!;
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||
{
|
||||
var count = buffer.ReadInt32();
|
||||
Paths = new ResourcePath[count];
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
Paths[i] = new ResourcePath(buffer.ReadString());
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||
{
|
||||
buffer.Write(Paths.Length);
|
||||
|
||||
foreach (var path in Paths)
|
||||
{
|
||||
buffer.Write(path.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
@@ -6,6 +6,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -18,7 +19,7 @@ namespace Robust.Shared.Prototypes
|
||||
/// Prototype that represents game entities.
|
||||
/// </summary>
|
||||
[Prototype("entity")]
|
||||
public class EntityPrototype : IPrototype, IIndexedPrototype, ISyncingPrototype
|
||||
public class EntityPrototype : IPrototype, ISyncingPrototype
|
||||
{
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
|
||||
@@ -257,6 +258,11 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Children.Clear();
|
||||
}
|
||||
|
||||
// Resolve inheritance.
|
||||
public bool Sync(IPrototypeManager manager, int stage)
|
||||
{
|
||||
@@ -424,6 +430,68 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateEntity(Entity entity)
|
||||
{
|
||||
bool HasBeenModified(string name, YamlMappingNode data, EntityPrototype prototype, IComponent currentComponent, IComponentFactory factory)
|
||||
{
|
||||
var component = factory.GetComponent(name);
|
||||
prototype.CurrentDeserializingComponent = name;
|
||||
ObjectSerializer ser = YamlObjectSerializer.NewReader(data, new PrototypeSerializationContext(prototype));
|
||||
component.ExposeData(ser);
|
||||
return component == (Component) currentComponent;
|
||||
}
|
||||
|
||||
if (Name != entity.Prototype?.Name)
|
||||
{
|
||||
Logger.Error($"Reloaded prototype used to update entity did not match entity's existing prototype: Expected '{entity.Prototype?.Name}', got '{Name}'");
|
||||
return;
|
||||
}
|
||||
|
||||
var factory = IoCManager.Resolve<IComponentFactory>();
|
||||
var componentManager = IoCManager.Resolve<IComponentManager>();
|
||||
var oldPrototype = entity.Prototype;
|
||||
|
||||
var oldPrototypeComponents = oldPrototype.Components.Keys
|
||||
.Where(n => n != "Transform" && n != "MetaData")
|
||||
.Select(name => (name, factory.GetRegistration(name).Type))
|
||||
.ToList();
|
||||
var newPrototypeComponents = Components.Keys
|
||||
.Where(n => n != "Transform" && n != "MetaData")
|
||||
.Select(name => (name, factory.GetRegistration(name).Type))
|
||||
.ToList();
|
||||
|
||||
var ignoredComponents = new List<string>();
|
||||
|
||||
// Find components to be removed, and remove them
|
||||
foreach (var (name, type) in oldPrototypeComponents.Except(newPrototypeComponents))
|
||||
{
|
||||
if (!HasBeenModified(name, oldPrototype.Components[name], oldPrototype, entity.GetComponent(type),
|
||||
factory) && Components.Keys.Contains(name))
|
||||
{
|
||||
ignoredComponents.Add(name);
|
||||
Logger.Debug(name);
|
||||
continue;
|
||||
}
|
||||
componentManager.RemoveComponent(entity.Uid, type);
|
||||
}
|
||||
componentManager.CullRemovedComponents();
|
||||
|
||||
// Add new components
|
||||
foreach (var (name, type) in newPrototypeComponents.Where(t => !ignoredComponents.Contains(t.name)).Except(oldPrototypeComponents))
|
||||
{
|
||||
var data = Components[name];
|
||||
var component = (Component) factory.GetComponent(name);
|
||||
ObjectSerializer ser = YamlObjectSerializer.NewReader(data, new PrototypeSerializationContext(this));
|
||||
CurrentDeserializingComponent = name;
|
||||
component.Owner = entity;
|
||||
component.ExposeData(ser);
|
||||
entity.AddComponent(component);
|
||||
}
|
||||
|
||||
// Update entity metadata
|
||||
entity.MetaData.EntityPrototype = this;
|
||||
}
|
||||
|
||||
internal static void LoadEntity(EntityPrototype? prototype, Entity entity, IComponentFactory factory, IEntityLoadContext? context)
|
||||
{
|
||||
YamlObjectSerializer.Context? defaultContext = null;
|
||||
|
||||
@@ -3,30 +3,24 @@
|
||||
namespace Robust.Shared.Prototypes
|
||||
{
|
||||
/// <summary>
|
||||
/// An IPrototype is a prototype that can be loaded from the global YAML prototypes.
|
||||
/// An IPrototype is a prototype that can be loaded from the global YAML prototypes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To use this, the prototype must be accessible through IoC with <see cref="IoCTargetAttribute"/>
|
||||
/// and it must have a <see cref="PrototypeAttribute"/> to give it a type string.
|
||||
/// To use this, the prototype must be accessible through IoC with <see cref="IoCTargetAttribute"/>
|
||||
/// and it must have a <see cref="PrototypeAttribute"/> to give it a type string.
|
||||
/// </remarks>
|
||||
public interface IPrototype
|
||||
{
|
||||
/// <summary>
|
||||
/// Load data from the YAML mappings in the prototype files.
|
||||
/// </summary>
|
||||
void LoadFrom(YamlMappingNode mapping);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension on <see cref="IPrototype"/> that allows it to be "indexed" by a string ID.
|
||||
/// </summary>
|
||||
public interface IIndexedPrototype
|
||||
{
|
||||
/// <summary>
|
||||
/// An ID for this prototype instance.
|
||||
/// If this is a duplicate, an error will be thrown.
|
||||
/// </summary>
|
||||
string ID { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Load data from the YAML mappings in the prototype files.
|
||||
/// </summary>
|
||||
void LoadFrom(YamlMappingNode mapping);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,6 +29,8 @@ namespace Robust.Shared.Prototypes
|
||||
/// </summary>
|
||||
public interface ISyncingPrototype
|
||||
{
|
||||
void Reset();
|
||||
|
||||
/// <summary>
|
||||
/// Sync and update cross-referencing data.
|
||||
/// Syncing works in stages, each time it will be called with the stage it's currently on.
|
||||
|
||||
@@ -4,11 +4,16 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.IoC.Exceptions;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.Core;
|
||||
@@ -21,6 +26,8 @@ namespace Robust.Shared.Prototypes
|
||||
/// </summary>
|
||||
public interface IPrototypeManager
|
||||
{
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Return an IEnumerable to iterate all prototypes of a certain type.
|
||||
/// </summary>
|
||||
@@ -28,6 +35,7 @@ namespace Robust.Shared.Prototypes
|
||||
/// Thrown if the type of prototype is not registered.
|
||||
/// </exception>
|
||||
IEnumerable<T> EnumeratePrototypes<T>() where T : class, IPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// Return an IEnumerable to iterate all prototypes of a certain type.
|
||||
/// </summary>
|
||||
@@ -35,32 +43,44 @@ namespace Robust.Shared.Prototypes
|
||||
/// Thrown if the type of prototype is not registered.
|
||||
/// </exception>
|
||||
IEnumerable<IPrototype> EnumeratePrototypes(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// Index for a <see cref="IIndexedPrototype"/> by ID.
|
||||
/// Index for a <see cref="IPrototype"/> by ID.
|
||||
/// </summary>
|
||||
/// <exception cref="KeyNotFoundException">
|
||||
/// Thrown if the type of prototype is not registered.
|
||||
/// </exception>
|
||||
T Index<T>(string id) where T : class, IIndexedPrototype;
|
||||
T Index<T>(string id) where T : class, IPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// Index for a <see cref="IIndexedPrototype"/> by ID.
|
||||
/// Index for a <see cref="IPrototype"/> by ID.
|
||||
/// </summary>
|
||||
/// <exception cref="KeyNotFoundException">
|
||||
/// Thrown if the ID does not exist or the type of prototype is not registered.
|
||||
/// </exception>
|
||||
IIndexedPrototype Index(Type type, string id);
|
||||
bool HasIndex<T>(string id) where T : IIndexedPrototype;
|
||||
bool TryIndex<T>(string id, out T prototype) where T : IIndexedPrototype;
|
||||
IPrototype Index(Type type, string id);
|
||||
bool HasIndex<T>(string id) where T : IPrototype;
|
||||
bool TryIndex<T>(string id, out T prototype) where T : IPrototype;
|
||||
|
||||
/// <summary>
|
||||
/// Load prototypes from files in a directory, recursively.
|
||||
/// </summary>
|
||||
void LoadDirectory(ResourcePath path);
|
||||
void LoadFromStream(TextReader stream);
|
||||
void LoadString(string str);
|
||||
Task<List<IPrototype>> LoadDirectory(ResourcePath path);
|
||||
|
||||
List<IPrototype> LoadFromStream(TextReader stream);
|
||||
|
||||
List<IPrototype> LoadString(string str);
|
||||
|
||||
/// <summary>
|
||||
/// Clear out all prototypes and reset to a blank slate.
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Performs a reload on all prototypes, updating the game state accordingly
|
||||
/// </summary>
|
||||
void ReloadPrototypes(ResourcePath file);
|
||||
|
||||
/// <summary>
|
||||
/// Syncs all inter-prototype data. Call this when operations adding new prototypes are done.
|
||||
/// </summary>
|
||||
@@ -79,7 +99,6 @@ namespace Robust.Shared.Prototypes
|
||||
void RegisterType(Type protoClass);
|
||||
|
||||
event Action<YamlStream, string>? LoadedData;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -103,25 +122,40 @@ namespace Robust.Shared.Prototypes
|
||||
{
|
||||
[Dependency] private readonly IReflectionManager ReflectionManager = default!;
|
||||
[Dependency] private readonly IDynamicTypeFactoryInternal _dynamicTypeFactory = default!;
|
||||
[Dependency] private readonly IResourceManager _resources = default!;
|
||||
[Dependency] public readonly IResourceManager Resources = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] public readonly ITaskManager TaskManager = default!;
|
||||
[Dependency] public readonly INetManager NetManager = default!;
|
||||
|
||||
private readonly Dictionary<string, Type> prototypeTypes = new();
|
||||
|
||||
private bool _initialized;
|
||||
private bool _hasEverBeenReloaded;
|
||||
private bool _hasEverResynced;
|
||||
|
||||
#region IPrototypeManager members
|
||||
private readonly Dictionary<Type, List<IPrototype>> prototypes = new();
|
||||
private readonly Dictionary<Type, Dictionary<string, IIndexedPrototype>> indexedPrototypes = new();
|
||||
private readonly Dictionary<Type, Dictionary<string, IPrototype>> prototypes = new();
|
||||
|
||||
private readonly HashSet<string> IgnoredPrototypeTypes = new();
|
||||
|
||||
public virtual void Initialize()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
throw new InvalidOperationException($"{nameof(PrototypeManager)} has already been initialized.");
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
public IEnumerable<T> EnumeratePrototypes<T>() where T : class, IPrototype
|
||||
{
|
||||
if (!_hasEverBeenReloaded)
|
||||
{
|
||||
throw new InvalidOperationException("No prototypes have been loaded yet.");
|
||||
}
|
||||
return prototypes[typeof(T)].Select((IPrototype p) => (T)p);
|
||||
|
||||
return prototypes[typeof(T)].Values.Select(p => (T) p);
|
||||
}
|
||||
|
||||
public IEnumerable<IPrototype> EnumeratePrototypes(Type type)
|
||||
@@ -130,10 +164,11 @@ namespace Robust.Shared.Prototypes
|
||||
{
|
||||
throw new InvalidOperationException("No prototypes have been loaded yet.");
|
||||
}
|
||||
return prototypes[type];
|
||||
|
||||
return prototypes[type].Values;
|
||||
}
|
||||
|
||||
public T Index<T>(string id) where T : class, IIndexedPrototype
|
||||
public T Index<T>(string id) where T : class, IPrototype
|
||||
{
|
||||
if (!_hasEverBeenReloaded)
|
||||
{
|
||||
@@ -141,7 +176,7 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
try
|
||||
{
|
||||
return (T)indexedPrototypes[typeof(T)][id];
|
||||
return (T)prototypes[typeof(T)][id];
|
||||
}
|
||||
catch (KeyNotFoundException)
|
||||
{
|
||||
@@ -149,24 +184,60 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
}
|
||||
|
||||
public IIndexedPrototype Index(Type type, string id)
|
||||
public IPrototype Index(Type type, string id)
|
||||
{
|
||||
if (!_hasEverBeenReloaded)
|
||||
{
|
||||
throw new InvalidOperationException("No prototypes have been loaded yet.");
|
||||
}
|
||||
return indexedPrototypes[type][id];
|
||||
|
||||
return prototypes[type][id];
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
prototypes.Clear();
|
||||
prototypeTypes.Clear();
|
||||
indexedPrototypes.Clear();
|
||||
prototypes.Clear();
|
||||
}
|
||||
|
||||
public virtual async void ReloadPrototypes(ResourcePath file)
|
||||
{
|
||||
#if !FULL_RELEASE
|
||||
var changed = await LoadFile(file.ToRootedPath(), true);
|
||||
Resync();
|
||||
|
||||
foreach (var prototype in changed)
|
||||
{
|
||||
if (prototype is not EntityPrototype entityPrototype)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var entity in _entityManager.GetEntities(new PredicateEntityQuery(e => e.Prototype != null && e.Prototype.ID == entityPrototype.ID)))
|
||||
{
|
||||
entityPrototype.UpdateEntity((Entity) entity);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Resync()
|
||||
{
|
||||
// TODO Make this smarter and only resync changed prototypes
|
||||
if (_hasEverResynced)
|
||||
{
|
||||
foreach (var prototypeList in prototypes.Values)
|
||||
{
|
||||
foreach (var prototype in prototypeList.Values)
|
||||
{
|
||||
if (prototype is ISyncingPrototype syncing)
|
||||
{
|
||||
syncing.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Type type in prototypeTypes.Values.Where(t => typeof(ISyncingPrototype).IsAssignableFrom(t)))
|
||||
{
|
||||
// This list is the list of prototypes we're syncing.
|
||||
@@ -178,17 +249,18 @@ namespace Robust.Shared.Prototypes
|
||||
// When we get to the end, do the whole thing again!
|
||||
// Yes this is ridiculously overengineered BUT IT PERFORMS WELL.
|
||||
// I hope.
|
||||
List<ISyncingPrototype> currentRun = prototypes[type].Select(p => (ISyncingPrototype)p).ToList();
|
||||
int stage = 0;
|
||||
List<ISyncingPrototype> currentRun = prototypes[type].Values.Select(p => (ISyncingPrototype) p).ToList();
|
||||
|
||||
var stage = 0;
|
||||
// Outer loop to iterate stages.
|
||||
while (currentRun.Count > 0)
|
||||
{
|
||||
// Increase positions to iterate over list.
|
||||
// If we need to stick, i gets reduced down below.
|
||||
for (int i = 0; i < currentRun.Count; i++)
|
||||
for (var i = 0; i < currentRun.Count; i++)
|
||||
{
|
||||
ISyncingPrototype prototype = currentRun[i];
|
||||
bool result = prototype.Sync(this, stage);
|
||||
var result = prototype.Sync(this, stage);
|
||||
// Keep prototype and move on to next one if it returns true.
|
||||
// Thus it stays in the list for next stage.
|
||||
if (result)
|
||||
@@ -205,85 +277,128 @@ namespace Robust.Shared.Prototypes
|
||||
stage++;
|
||||
}
|
||||
}
|
||||
|
||||
_hasEverResynced = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadDirectory(ResourcePath path)
|
||||
public async Task<List<IPrototype>> LoadDirectory(ResourcePath path)
|
||||
{
|
||||
var sawmill = Logger.GetSawmill("eng");
|
||||
var changedPrototypes = new List<IPrototype>();
|
||||
|
||||
_hasEverBeenReloaded = true;
|
||||
var yamlStreams = _resources.ContentFindFiles(path).ToList().AsParallel()
|
||||
.Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith("."))
|
||||
.Select(filePath =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(_resources.ContentFileRead(filePath), EncodingHelpers.UTF8);
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
var streams = Resources.ContentFindFiles(path).ToList().AsParallel()
|
||||
.Where(filePath => filePath.Extension == "yml" && !filePath.Filename.StartsWith("."));
|
||||
|
||||
var result = ((YamlStream? yamlStream, ResourcePath?))(yamlStream, filePath);
|
||||
|
||||
LoadedData?.Invoke(yamlStream, filePath.ToString());
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
sawmill.Error("YamlException whilst loading prototypes from {0}: {1}", filePath, e.Message);
|
||||
return (null, null);
|
||||
}
|
||||
})
|
||||
.Where(p => p.yamlStream != null) // Filter out loading errors.
|
||||
.ToList();
|
||||
|
||||
foreach (var (stream, filePath) in yamlStreams)
|
||||
foreach (var resourcePath in streams)
|
||||
{
|
||||
for (var i = 0; i < stream!.Documents.Count; i++)
|
||||
var filePrototypes = await LoadFile(resourcePath);
|
||||
changedPrototypes.AddRange(filePrototypes);
|
||||
}
|
||||
|
||||
return changedPrototypes;
|
||||
}
|
||||
|
||||
private Task<StreamReader?> ReadFile(ResourcePath file, bool @throw = true)
|
||||
{
|
||||
var retries = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
try
|
||||
var reader = new StreamReader(Resources.ContentFileRead(file), EncodingHelpers.UTF8);
|
||||
|
||||
return Task.FromResult<StreamReader?>(reader);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
if (retries > 10)
|
||||
{
|
||||
LoadFromDocument(stream.Documents[i]);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("eng", $"Exception whilst loading prototypes from {filePath}#{i}:\n{e}");
|
||||
if (@throw)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
Logger.Error($"Error reloading prototypes in file {file}.", e);
|
||||
return Task.FromResult<StreamReader?>(null);
|
||||
}
|
||||
|
||||
retries++;
|
||||
Thread.Sleep(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadFromStream(TextReader stream)
|
||||
public async Task<List<IPrototype>> LoadFile(ResourcePath file, bool overwrite = false)
|
||||
{
|
||||
var changedPrototypes = new List<IPrototype>();
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = await ReadFile(file, !overwrite);
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
|
||||
LoadedData?.Invoke(yamlStream, file.ToString());
|
||||
|
||||
for (var i = 0; i < yamlStream.Documents.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var documentPrototypes = LoadFromDocument(yamlStream.Documents[i], overwrite);
|
||||
changedPrototypes.AddRange(documentPrototypes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.ErrorS("eng", $"Exception whilst loading prototypes from {file}#{i}:\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
var sawmill = Logger.GetSawmill("eng");
|
||||
sawmill.Error("YamlException whilst loading prototypes from {0}: {1}", file, e.Message);
|
||||
}
|
||||
|
||||
return changedPrototypes;
|
||||
}
|
||||
|
||||
public List<IPrototype> LoadFromStream(TextReader stream)
|
||||
{
|
||||
var changedPrototypes = new List<IPrototype>();
|
||||
_hasEverBeenReloaded = true;
|
||||
var yaml = new YamlStream();
|
||||
yaml.Load(stream);
|
||||
|
||||
for (int i = 0; i < yaml.Documents.Count; i++)
|
||||
for (var i = 0; i < yaml.Documents.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadFromDocument(yaml.Documents[i]);
|
||||
var documentPrototypes = LoadFromDocument(yaml.Documents[i]);
|
||||
changedPrototypes.AddRange(documentPrototypes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new PrototypeLoadException(string.Format("Failed to load prototypes from document#{0}", i), e);
|
||||
throw new PrototypeLoadException($"Failed to load prototypes from document#{i}", e);
|
||||
}
|
||||
}
|
||||
|
||||
LoadedData?.Invoke(yaml, "anonymous prototypes YAML stream");
|
||||
|
||||
return changedPrototypes;
|
||||
}
|
||||
|
||||
public void LoadString(string str)
|
||||
public List<IPrototype> LoadString(string str)
|
||||
{
|
||||
LoadFromStream(new StreamReader(str));
|
||||
return LoadFromStream(new StreamReader(str));
|
||||
}
|
||||
|
||||
#endregion IPrototypeManager members
|
||||
|
||||
public void PostInject()
|
||||
{
|
||||
ReflectionManager.OnAssemblyAdded += (_, __) => ReloadPrototypeTypes();
|
||||
ReflectionManager.OnAssemblyAdded += (_, _) => ReloadPrototypeTypes();
|
||||
ReloadPrototypeTypes();
|
||||
}
|
||||
|
||||
@@ -296,9 +411,11 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFromDocument(YamlDocument document)
|
||||
private List<IPrototype> LoadFromDocument(YamlDocument document, bool overwrite = false)
|
||||
{
|
||||
var rootNode = (YamlSequenceNode)document.RootNode;
|
||||
var changedPrototypes = new List<IPrototype>();
|
||||
var rootNode = (YamlSequenceNode) document.RootNode;
|
||||
|
||||
foreach (YamlMappingNode node in rootNode.Cast<YamlMappingNode>())
|
||||
{
|
||||
var type = node.GetNode("type").AsString();
|
||||
@@ -308,38 +425,41 @@ namespace Robust.Shared.Prototypes
|
||||
{
|
||||
continue;
|
||||
}
|
||||
throw new PrototypeLoadException(string.Format("Unknown prototype type: '{0}'", type));
|
||||
|
||||
throw new PrototypeLoadException($"Unknown prototype type: '{type}'");
|
||||
}
|
||||
|
||||
var prototypeType = prototypeTypes[type];
|
||||
var prototype = _dynamicTypeFactory.CreateInstanceUnchecked<IPrototype>(prototypeType);
|
||||
|
||||
prototype.LoadFrom(node);
|
||||
prototypes[prototypeType].Add(prototype);
|
||||
var indexedPrototype = prototype as IIndexedPrototype;
|
||||
if (indexedPrototype != null)
|
||||
changedPrototypes.Add(prototype);
|
||||
|
||||
var id = prototype.ID;
|
||||
|
||||
if (!overwrite && prototypes[prototypeType].ContainsKey(id))
|
||||
{
|
||||
var id = indexedPrototype.ID;
|
||||
if (indexedPrototypes[prototypeType].ContainsKey(id))
|
||||
{
|
||||
throw new PrototypeLoadException(string.Format("Duplicate ID: '{0}'", id));
|
||||
}
|
||||
indexedPrototypes[prototypeType][id] = (IIndexedPrototype)prototype;
|
||||
throw new PrototypeLoadException($"Duplicate ID: '{id}'");
|
||||
}
|
||||
|
||||
prototypes[prototypeType][id] = prototype;
|
||||
}
|
||||
|
||||
return changedPrototypes;
|
||||
}
|
||||
|
||||
public bool HasIndex<T>(string id) where T : IIndexedPrototype
|
||||
public bool HasIndex<T>(string id) where T : IPrototype
|
||||
{
|
||||
if (!indexedPrototypes.TryGetValue(typeof(T), out var index))
|
||||
if (!prototypes.TryGetValue(typeof(T), out var index))
|
||||
{
|
||||
throw new UnknownPrototypeException(id);
|
||||
}
|
||||
return index.ContainsKey(id);
|
||||
}
|
||||
|
||||
public bool TryIndex<T>(string id, [MaybeNullWhen(false)] out T prototype) where T : IIndexedPrototype
|
||||
public bool TryIndex<T>(string id, [MaybeNullWhen(false)] out T prototype) where T : IPrototype
|
||||
{
|
||||
if (!indexedPrototypes.TryGetValue(typeof(T), out var index))
|
||||
if (!prototypes.TryGetValue(typeof(T), out var index))
|
||||
{
|
||||
throw new UnknownPrototypeException(id);
|
||||
}
|
||||
@@ -376,11 +496,10 @@ namespace Robust.Shared.Prototypes
|
||||
}
|
||||
|
||||
prototypeTypes[attribute.Type] = type;
|
||||
prototypes[type] = new List<IPrototype>();
|
||||
|
||||
if (typeof(IIndexedPrototype).IsAssignableFrom(type))
|
||||
if (typeof(IPrototype).IsAssignableFrom(type))
|
||||
{
|
||||
indexedPrototypes[type] = new Dictionary<string, IIndexedPrototype>();
|
||||
prototypes[type] = new Dictionary<string, IPrototype>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,11 +523,6 @@ namespace Robust.Shared.Prototypes
|
||||
public PrototypeLoadException(SerializationInfo info, StreamingContext context) : base(info, context)
|
||||
{
|
||||
}
|
||||
|
||||
public override void GetObjectData(SerializationInfo info, StreamingContext context)
|
||||
{
|
||||
base.GetObjectData(info, context);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace Robust.Shared.Serialization
|
||||
[PublicAPI]
|
||||
internal interface IRobustMappedStringSerializer
|
||||
{
|
||||
bool Locked { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type serializer to register with NetSerializer.
|
||||
/// </summary>
|
||||
|
||||
@@ -133,6 +133,7 @@ namespace Robust.Shared.Serialization
|
||||
| RegexOptions.IgnorePatternWhitespace
|
||||
);
|
||||
|
||||
public bool Locked => _dict.Locked;
|
||||
|
||||
public ITypeSerializer TypeSerializer => this;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user