Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Services/EventService.cs
NotAlwaysTrue 9b35f6b23f Sync with upstream
* Update bug-reports.yml

* Fix modifyChatMessage hook

* Add LuaCsSetup.Lua back for compatibility

* Fix Game.AssignOnExecute having command arguments be passed as varargs instead of a table

* Actually use the PackageId const everywhere we need to refer to our content package

* Load languages files even if the package is disabled

* Fix Hook.Remove not being implemented properly

* - Changed event aliases to be case insensitive.

* - Fixed assembly logging style.
- Fixed double logging during execution.

* Fix garbage network data being read by the game when reading LuaCs network messages

* PackageId -> PackageName

* Added caching toggle to PluginManagementService

* Fix LuaCs initializing too late for singleplayer campaigns and rework the C# prompt to only show when enabling mods/joining server

* Oops, fix NRE crash

* Fix hide username in logs config not doing anything

* Fix Cs prompt showing up more than one between rounds

* Fix server host being prompted twice with the C# popup

* Ignore our workshop packages from the game's dependency thing since it doesn't really make sense

* Load console commands after executing and possible fix for the not console command permitted

* Added fallback friendly name resolution for ModConfig assembly contents.

* Register Voronoi2 stuff

* Added configinfo null check to SettingBase.cs

* Add safety check so this stops crashing when we look at it the wrong way

* Fixed "Folder" attribute files not being found.

* Keep the LuaCsConfig class laying around for compatibility, not sure anywhere in our code base (and shouldn't be)

* Added fallback compilation for UseInternalsAwareAssembly if the publicized script compilation fails.

* Added legacy overload of AddCommand for mod compat.

* Added LoggerService to Lua env. Made ILoggerService compliant with LuaCsLogger API.

* Changed csharp script compilation algorithm to be best effort.

* Added "RunUnrestricted" mode for lua scripts that need to run outside of sandbox.

* - Fixed networking sync vars failing to sync initially.
- Fixed lua failing to differentiate overloads ISettingBase.

* Add alias for human.CPRSuccess and human.CPRFailed

* - Fixed up the settings menu.
- Made SettingEntry throw an error if "Value" attribute is not found in XML.
- Fixed saved values for settings sometimes not reloading after disabling and re-enabling a package.

* Fix LuaCs net messages received during connection initialization to be read incorrectly, happened because we would reset the BitPosition in our harmony patch which would cause the message to be read incorrectly later

* Allow reloadlua to force the state to running

* New icon for settings and make the top left text more user friendly

* Fix client.packages hook sending normal packages

* Fixed OnUpdate() not passing in deltaTime instead of totalTime.

* Missing diffs from bb21a09244

* Added networking tests for configs.

* Added missing diffs for f61f852a25.

* Some tweaks to the text

* Remove missing Value error, it should just use the default value if it's not specified

* Fix UseInternalAccessName

* Always purge cashes for plugin content on unloading.

* Fix texture not multiple of 4

* v1.12.7.0 (Spring Update 2026 Hotfix 1)

---------

Co-authored-by: Joonas Rikkonen <poe.regalis@gmail.com>
Co-authored-by: Evil Factory <36804725+evilfactory@users.noreply.github.com>
Co-authored-by: MapleWheels <njainanan@hotmail.com>
2026-04-25 12:10:24 +08:00

427 lines
16 KiB
C#

using Barotrauma.LuaCs.Events;
using FluentResults;
using Microsoft.Toolkit.Diagnostics;
using MoonSharp.Interpreter;
using OneOf;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Barotrauma.LuaCs;
public partial class EventService : IEventService
{
private readonly record struct TypeStringKey : IEqualityComparer<TypeStringKey>, IEquatable<TypeStringKey>
{
public Type Type { get; init; }
public string TypeName { get; init; }
public readonly int HashCode;
public TypeStringKey(Type type)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
TypeName = type.Name.ToLowerInvariant();
HashCode = TypeName.GetHashCode();
}
public TypeStringKey(string typeName)
{
Type = null;
TypeName = typeName?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(typeName));
HashCode = TypeName.GetHashCode();
}
public bool Equals(TypeStringKey x, TypeStringKey y)
{
if (x.Type is not null && y.Type is not null)
return x.Type == y.Type;
return x.TypeName == y.TypeName;
}
public int GetHashCode(TypeStringKey obj)
{
return obj.HashCode;
}
public static implicit operator TypeStringKey(Type type) => new(type);
public static implicit operator TypeStringKey(string typeName) => new(typeName);
}
private readonly ILoggerService _loggerService;
private readonly ILuaPatcher _luaPatcher;
private readonly AsyncReaderWriterLock _operationsLock = new();
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<OneOf<IEvent, string>, IEvent>> _subscribers = new();
private readonly ConcurrentDictionary<TypeStringKey, (TypeStringKey Event, Func<LuaCsFunc, IEvent> RunnerFactory)> _luaAliasEventFactory = new();
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<TypeStringKey, LuaCsFunc>> _luaLegacyEventsSubscribers = new();
private readonly ConcurrentDictionary<IEventService, IEventService> _subscribedEventDispatchers = new();
#region LifeCycle
public void Dispose()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_luaLegacyEventsSubscribers.Clear();
_luaAliasEventFactory.Clear();
_subscribers.Clear();
_luaPatcher.Dispose();
}
private int _isDisposed;
public EventService(ILoggerService loggerService, ILuaPatcher luaPatcher)
{
_loggerService = loggerService;
_luaPatcher = luaPatcher;
}
public bool IsDisposed
{
get => ModUtils.Threading.GetBool(ref _isDisposed);
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
}
public FluentResults.Result Reset()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_luaLegacyEventsSubscribers.Clear();
_luaAliasEventFactory.Clear();
_subscribers.Clear();
_luaPatcher.Reset();
return FluentResults.Result.Ok();
}
#endregion
#region LuaEventSystem
public void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
Guard.IsNotNull(callback, nameof(callback));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
{
var eventSubs = _subscribers.GetOrAdd(eventFunc.Event, key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
eventSubs[identifier] = eventFunc.RunnerFactory(callback);
}
else
{
var eventSubs = _luaLegacyEventsSubscribers.GetOrAdd(eventName, key => new ConcurrentDictionary<TypeStringKey, LuaCsFunc>());
eventSubs[identifier] = callback;
}
}
public void Add(string eventName, LuaCsFunc callback, object owner = null)
{
// random ident, we hope for no conflicts :barodev:.
Add(eventName, Random.Shared.NextInt64().ToString() ,callback);
}
public object Call(string eventName, params object[] args)
{
return Call<object>(eventName, args);
}
[MoonSharpHidden] // Needs to be hidden so Lua doesn't accidentally use this instead of the above
public T Call<T>(string eventName, params object[] args)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubscribers)
|| eventSubscribers.IsEmpty)
{
return default;
}
T returnValue = default;
foreach (var subscriber in eventSubscribers)
{
try
{
object result = subscriber.Value.Invoke(args);
if (result is DynValue luaResult)
{
if (luaResult.Type == DataType.Tuple)
{
bool replaceNil = luaResult.Tuple.Length > 1 && luaResult.Tuple[1].CastToBool();
if (!luaResult.Tuple[0].IsNil() || replaceNil)
{
returnValue = luaResult.ToObject<T>();
}
}
else if (!luaResult.IsNil())
{
returnValue = luaResult.ToObject<T>();
}
}
else
{
returnValue = (T)result;
}
}
catch (Exception e)
{
_loggerService.LogError(e.Message);
#if DEBUG
throw;
#endif
}
}
return returnValue;
}
public void Subscribe<T>(string identifier, IDictionary<string, LuaCsFunc> callbacks) where T : class, IEvent<T>
{
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
Guard.IsNotNull(callbacks, nameof(callbacks));
Guard.IsNotEmpty(callbacks, nameof(callbacks));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var eventSubs = _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
eventSubs[identifier] = T.GetLuaRunner(callbacks);
}
public void Remove(string eventName, string identifier)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
{
if (_subscribers.TryGetValue(eventFunc.Event, out var eventSubs))
{
eventSubs.TryRemove(identifier, out _);
}
}
else
{
if (_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubs))
{
eventSubs.TryRemove(identifier, out _);
}
}
}
public void Unsubscribe(string eventName, string identifier)
{
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
if (!_subscribers.TryGetValue(eventName, out var evtSubscribers))
{
return;
}
evtSubscribers.TryRemove(identifier, out _);
}
public void PublishLuaEvent<T>(LuaCsFunc subscriberRunner) where T : class, IEvent<T>
{
this.PublishEvent<T>(sub => subscriberRunner(sub));
}
public FluentResults.Result RegisterLuaEventAlias<T>(string luaEventName, string targetMethod) where T : class, IEvent<T>
{
Guard.IsNotNullOrWhiteSpace(luaEventName, nameof(luaEventName));
Guard.IsNotNullOrWhiteSpace(targetMethod, nameof(targetMethod));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (_luaAliasEventFactory.ContainsKey(luaEventName))
{
#if DEBUG
ThrowHelper.ThrowInvalidOperationException($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
#endif
return FluentResults.Result.Fail($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
}
var eventRunnerFactory = (LuaCsFunc function) => (IEvent)T.GetLuaRunner(new Dictionary<string, LuaCsFunc>
{
{ targetMethod, function }
});
_luaAliasEventFactory[luaEventName] = (Event: typeof(T), RunnerFactory: eventRunnerFactory);
// create the group
_subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
return FluentResults.Result.Ok();
}
#endregion
public FluentResults.Result Subscribe<T>(T subscriber) where T : class, IEvent<T>
{
Guard.IsNotNull(subscriber, nameof(subscriber));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
var eventSubs =
_subscribers.GetOrAdd(typeof(T), (type) => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
if (eventSubs.ContainsKey(subscriber))
{
ThrowHelper.ThrowInvalidOperationException($"{nameof(Subscribe)}: The instance is already registered!");
}
return eventSubs.TryAdd(subscriber, subscriber)
? FluentResults.Result.Ok()
: FluentResults.Result.Fail($"{nameof(Subscribe)}: Failed to add subscriber.");
}
public void Unsubscribe<T>(T subscriber) where T : class, IEvent
{
Guard.IsNotNull(subscriber, nameof(subscriber));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_subscribers.TryGetValue(typeof(T), out var evtSubscribers))
{
return;
}
evtSubscribers.TryRemove(subscriber, out _);
}
public void ClearAllEventSubscribers<T>() where T : class, IEvent
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribers.TryRemove(typeof(T), out _);
}
public void ClearAllSubscribers()
{
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribers.Clear();
}
public FluentResults.Result PublishEvent<T>(Action<T> action) where T : class, IEvent<T>
{
Guard.IsNotNull(action, nameof(action));
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
if (!_subscribers.TryGetValue(typeof(T), out var subs) || subs.IsEmpty)
{
return FluentResults.Result.Ok();
}
var results = new FluentResults.Result();
foreach (var sub in subs)
{
try
{
action.Invoke(Unsafe.As<T>(sub.Value));
}
catch (Exception e)
{
results.WithError(new ExceptionalError(e));
_loggerService.LogError(e.Message);
continue;
}
}
foreach (var dispatchers in _subscribedEventDispatchers.ToImmutableArray())
{
dispatchers.Value.PublishEvent(action);
}
return results;
}
public void AddDispatcherEventService(IEventService eventService)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribedEventDispatchers.TryAdd(eventService, eventService);
}
public void RemoveDispatcherEventService(IEventService eventService)
{
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
IService.CheckDisposed(this);
_subscribedEventDispatchers.TryRemove(eventService, out _);
}
#region LuaPatcherAdapter
public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(identifier, className, methodName, parameterTypes, patch, hookType);
}
public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(identifier, className, methodName, patch, hookType);
}
public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(className, methodName, parameterTypes, patch, hookType);
}
public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
return _luaPatcher.Patch(className, methodName, patch, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType)
{
return _luaPatcher.RemovePatch(className, className, methodName, parameterTypes, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType)
{
return _luaPatcher.RemovePatch(className, className, methodName, hookType);
}
public void HookMethod(string identifier, MethodBase method, LuaCsPatch patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null)
{
_luaPatcher.HookMethod(identifier, method, patch, hookType, owner);
}
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(identifier, className, methodName, parameterNames, patch, hookMethodType);
}
public void HookMethod(string identifier, string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(identifier, className, methodName, patch, hookMethodType);
}
public void HookMethod(string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(className, methodName, patch, hookMethodType);
}
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
{
_luaPatcher.HookMethod(className, methodName, parameterNames, patch, hookMethodType);
}
#endregion
}