Skip to content

Commit 5f360cb

Browse files
committed
Support for LibraryPlugin, bump 3.3.0
1 parent 309eb0d commit 5f360cb

9 files changed

Lines changed: 179 additions & 45 deletions

File tree

ETS2LA.Backend/EventBus/Bus.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using ETS2LA.Logging;
12
using ETS2LA.Shared;
23

34
namespace ETS2LA.Backend.Events
@@ -37,12 +38,12 @@ public void Publish<T>(string topic, T data)
3738
{
3839
foreach (var handler in _subscribers[topic])
3940
{
40-
if (handler is Action<T> action)
41-
{
42-
action.Invoke(data);
43-
}
41+
handler.DynamicInvoke(data);
4442
}
45-
} catch { }
43+
} catch (Exception ex)
44+
{
45+
Logger.Error($"Error while publishing event on topic '{topic}': {ex}");
46+
}
4647
}
4748
}
4849
}

ETS2LA.Backend/PluginHandler/Handler.cs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
using ETS2LA.Backend.Events;
44
using ETS2LA.Notifications;
55
using System.Runtime.Loader;
6+
using System.Reflection;
67

78
namespace ETS2LA.Backend
89
{
10+
// The class instance for this lives in PluginBackend
911
public class PluginHandler
1012
{
1113
// These are files in the plugins folder that the backend will
@@ -19,6 +21,7 @@ public class PluginHandler
1921
};
2022

2123
public readonly List<IPlugin> LoadedPlugins = new();
24+
public readonly List<ILibraryPlugin> LoadedLibraryPlugins = new();
2225

2326
// Check PluginLoadContext.cs for why we need to keep track of them here.
2427
// TLDR: To be able to reload assemblies without restarting ETS2LA.
@@ -32,12 +35,12 @@ public class PluginHandler
3235
public Action<IPlugin>? PluginEnabled;
3336
public Action<IPlugin>? PluginDisabled;
3437
public bool loading = false;
35-
36-
public string[] DiscoverPlugins()
38+
39+
public string[] DiscoverDlls(string path)
3740
{
3841
try
3942
{
40-
var pluginFiles = Directory.GetFiles("Plugins", "*.dll");
43+
var pluginFiles = Directory.GetFiles(path, "*.dll");
4144

4245
// Exclude anything in _exclusions.
4346
pluginFiles = pluginFiles.Where(file =>
@@ -52,15 +55,41 @@ public string[] DiscoverPlugins()
5255
return pluginFiles;
5356
} catch (Exception ex)
5457
{
55-
Logger.Error($"Failed to discover plugins: {ex.Message}");
58+
Logger.Error($"Failed to discover Dlls: {ex.Message}");
5659
return Array.Empty<string>();
5760
}
5861
}
5962

63+
public void LoadLibraries()
64+
{
65+
string[] libraryFiles = DiscoverDlls("Libraries");
66+
Logger.Info($"Discovered {libraryFiles.Length} .dll files in Libraries folder.");
67+
foreach (string filename in libraryFiles)
68+
{
69+
try
70+
{
71+
var assembly = Assembly.LoadFrom(filename);
72+
var libraryTypes = assembly.GetTypes()
73+
.Where(t => typeof(ILibraryPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
74+
75+
foreach (var type in libraryTypes)
76+
{
77+
var libraryPlugin = (ILibraryPlugin)Activator.CreateInstance(type)!;
78+
LoadedLibraryPlugins.Add(libraryPlugin);
79+
Logger.Info($"Loaded library plugin: [gray]{type.FullName}[/] from [gray]{filename}[/].");
80+
}
81+
}
82+
catch (Exception ex)
83+
{
84+
Logger.Error($"Failed to load library plugin from [gray]{filename}[/]: {ex}");
85+
}
86+
}
87+
}
88+
6089
public void LoadPlugins()
6190
{
6291
loading = true;
63-
string[] pluginFiles = DiscoverPlugins();
92+
string[] pluginFiles = DiscoverDlls("Plugins");
6493
Logger.Info($"Discovered {pluginFiles.Length} .dll files in Plugin folder.");
6594
foreach (string filename in pluginFiles)
6695
{
@@ -237,6 +266,11 @@ private void CleanupShadowDirectory(AssemblyLoadContext context)
237266
return LoadedPlugins.FirstOrDefault(p => p.Info.Id == pluginId);
238267
}
239268

269+
private ILibraryPlugin? GetLibraryPluginById(string pluginId)
270+
{
271+
return LoadedLibraryPlugins.FirstOrDefault(p => p.Info.Id == pluginId);
272+
}
273+
240274
public bool EnablePlugin(IPlugin? plugin = null, string? pluginId = null)
241275
{
242276
plugin ??= GetPluginById(pluginId!);
@@ -251,17 +285,20 @@ public bool EnablePlugin(IPlugin? plugin = null, string? pluginId = null)
251285
{
252286
var dependency = GetPluginById(dependencyId);
253287
if (dependency == null) {
254-
NotificationHandler.Current.SendNotification(new Notification
288+
if (GetLibraryPluginById(dependencyId) == null)
255289
{
256-
Id = $"Backend.PluginHandler.MissingDependency.{plugin.Info.Id}",
257-
Title = $"{plugin.Info.Name}",
258-
Content = $"Missing dependency: {dependencyId}",
259-
Level = NotificationLevel.Danger
260-
});
261-
Logger.Warn($"Cannot enable plugin {plugin.Info.Name} because dependency {dependencyId} was not found.");
262-
return false;
290+
NotificationHandler.Current.SendNotification(new Notification
291+
{
292+
Id = $"Backend.PluginHandler.MissingDependency.{plugin.Info.Id}",
293+
Title = $"{plugin.Info.Name}",
294+
Content = $"Missing dependency: {dependencyId}",
295+
Level = NotificationLevel.Danger
296+
});
297+
Logger.Warn($"Cannot enable plugin {plugin.Info.Name} because dependency {dependencyId} was not found.");
298+
return false;
299+
}
263300
}
264-
if (!dependency._IsRunning)
301+
if (dependency != null && !dependency._IsRunning)
265302
{
266303
var success = EnablePlugin(dependency);
267304
if (!success) {

ETS2LA.Backend/Program.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class PluginBackend
2525
/// <summary>
2626
/// The PluginHandler is what actually manages the plugins.
2727
/// </summary>
28-
public PluginHandler? pluginHandler;
28+
public PluginHandler? PluginHandler;
2929
/// <summary>
3030
/// This event is fired when the backend has been loaded.
3131
/// </summary>
@@ -39,8 +39,9 @@ public void Start()
3939
{
4040
Logger.Console.Status().Start("Starting ETS2LA...", ctx =>
4141
{
42-
pluginHandler = new PluginHandler();
43-
pluginHandler.LoadPlugins();
42+
PluginHandler = new PluginHandler();
43+
PluginHandler.LoadLibraries();
44+
PluginHandler.LoadPlugins();
4445
Thread.Sleep(1000);
4546

4647
Logger.Success("ETS2LA is running.");
@@ -51,9 +52,9 @@ public void Start()
5152

5253
public void Shutdown()
5354
{
54-
if (pluginHandler != null)
55+
if (PluginHandler != null)
5556
{
56-
pluginHandler.UnloadPlugins();
57+
PluginHandler.UnloadPlugins();
5758
}
5859
ControlsBackend.Current.Shutdown();
5960
AudioHandler.Current.Shutdown();

ETS2LA.Game/Data/Classes.cs

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,7 @@ public int GetBestLaneFor(Vector3 Position, bool inverted = false)
262262
float closestFactor = GetFactorForPoint(Position);
263263
if (inverted) closestFactor = 1 - closestFactor;
264264

265-
int closestLane = 0;
265+
int closestLane = -1;
266266
float closestLaneDistance = float.MaxValue;
267267
for (int i = 0; i < GetLaneCount(Side.Left); i++)
268268
{
@@ -335,9 +335,9 @@ public OrientedPoint InterpolateLane(float t, Side side, int laneIndex, float ad
335335
{
336336
if (t < 0 || t > 1) throw new ArgumentOutOfRangeException(nameof(t), "t must be between 0 and 1");
337337
if (side == Side.Left && (laneIndex < 0 || laneIndex >= LeftLaneOffsetsEnd.Length))
338-
throw new ArgumentOutOfRangeException(nameof(laneIndex), $"laneIndex must be between 0 and {LeftLaneOffsetsEnd.Length - 1} for left side");
338+
throw new ArgumentOutOfRangeException(nameof(laneIndex), $"laneIndex ({laneIndex}) must be between 0 and {LeftLaneOffsetsEnd.Length - 1} for left side");
339339
if (side == Side.Right && (laneIndex < 0 || laneIndex >= RightLaneOffsetsEnd.Length))
340-
throw new ArgumentOutOfRangeException(nameof(laneIndex), $"laneIndex must be between 0 and {RightLaneOffsetsEnd.Length - 1} for right side");
340+
throw new ArgumentOutOfRangeException(nameof(laneIndex), $"laneIndex ({laneIndex}) must be between 0 and {RightLaneOffsetsEnd.Length - 1} for right side");
341341

342342
float offset = side == Side.Left ? LeftLaneOffsetsEnd[laneIndex] : RightLaneOffsetsEnd[laneIndex];
343343
float lastOffset = side == Side.Left ? (LeftLaneOffsetsStart != null ? LeftLaneOffsetsStart[laneIndex] : offset)
@@ -460,12 +460,61 @@ public OrientedPoint InterpolateBetweenLanesDist(float dist, Side side, float la
460460
/// <returns>Factor from 0-1 along the road.</returns>
461461
public float GetFactorForPoint(Vector3 point)
462462
{
463-
Vector3 ab = Road.Node.Position - Road.ForwardNode.Position;
464-
float lengthSquared = Vector3.Dot(ab, ab);
465-
if (lengthSquared == 0) return 0;
463+
// The road is split into N segments, then we find the best segment and do
464+
// a projection to that. After that there's a slight refining step to avoid twiching
465+
// when going from segment to segment.
466+
467+
// The reason we can't project across the start / end points, is that on curved roads that will
468+
// result in incorrect projections. Imagine a 180 degree curve, if we project across the start / end
469+
// you'll result in a half circle, where the start and end move "faster" than the middle.
466470

467-
float t = Vector3.Dot(point - Road.ForwardNode.Position, ab) / lengthSquared;
468-
return Math.Clamp(t, 0, 1);
471+
const int SEGMENTS = 8;
472+
const float POINT_DIST = 1f / SEGMENTS;
473+
474+
float bestT = 0;
475+
float minDistanceSq = float.MaxValue;
476+
477+
Vector3 prevPoint = Interpolate(0).Position;
478+
for (int i = 0; i < SEGMENTS; i++)
479+
{
480+
Vector3 nextPoint = Interpolate((i + 1) * POINT_DIST).Position;
481+
Vector3 v = nextPoint - prevPoint;
482+
float lenSq = Vector3.Dot(v, v);
483+
484+
if (lenSq > 0)
485+
{
486+
float tLocal = Math.Clamp(Vector3.Dot(point - prevPoint, v) / lenSq, 0, 1);
487+
Vector3 projected = prevPoint + tLocal * v;
488+
float distSq = Vector3.DistanceSquared(point, projected);
489+
490+
if (distSq < minDistanceSq)
491+
{
492+
minDistanceSq = distSq;
493+
bestT = (i + tLocal) * POINT_DIST;
494+
}
495+
}
496+
prevPoint = nextPoint;
497+
}
498+
499+
// Four iterations around bestT
500+
float searchRange = POINT_DIST;
501+
for (int r = 0; r < 4; r++)
502+
{
503+
float step = searchRange * 0.25f;
504+
float t1 = Math.Clamp(bestT - step, 0, 1);
505+
float t2 = Math.Clamp(bestT + step, 0, 1);
506+
507+
float d1 = Vector3.DistanceSquared(point, Interpolate(t1).Position);
508+
float dMid = Vector3.DistanceSquared(point, Interpolate(bestT).Position);
509+
float d2 = Vector3.DistanceSquared(point, Interpolate(t2).Position);
510+
511+
if (d1 < dMid && d1 < d2) { bestT = t1; }
512+
else if (d2 < dMid) { bestT = t2; }
513+
514+
searchRange *= 0.5f;
515+
}
516+
517+
return bestT;
469518
}
470519

471520
/// <summary>
@@ -596,13 +645,30 @@ public ParsedRoad GetParsedRoadForFactor(float factor)
596645
float accumulatedLength = 0;
597646
foreach (var road in Roads)
598647
{
599-
if (accumulatedLength + road.Road.Length >= distance)
648+
if (accumulatedLength + road.Road.Length >= distance - 1f)
600649
return road;
601650
accumulatedLength += road.Road.Length;
602651
}
603652
return Roads[Roads.Count - 1];
604653
}
605654

655+
public float FactorToRoadFactor(float factor, ParsedRoad road)
656+
{
657+
float distance = factor * TotalLength;
658+
float accumulatedLength = 0;
659+
foreach (var r in Roads)
660+
{
661+
if (accumulatedLength + r.Road.Length >= distance - 1f)
662+
{
663+
float localDistance = distance - accumulatedLength;
664+
float localFactor = localDistance / r.Road.Length;
665+
return localFactor;
666+
}
667+
accumulatedLength += r.Road.Length;
668+
}
669+
return 1f; // Return 1f if no road is found (should not happen)
670+
}
671+
606672
/// <summary>
607673
/// Get the best lane for a specific position. Negative lanes indicate left-side lanes, while positives <br/>
608674
/// right side lanes. Each lane is from 1 to X (so -1, -2, -3 etc...)
@@ -695,6 +761,7 @@ public OrientedPoint InterpolateLane(float t, Side side, int laneIndex, float ad
695761
throw new ArgumentOutOfRangeException(nameof(laneIndex), $"laneIndex must be between 0 and {GetLaneCount(Side.Right) - 1} for right side");
696762

697763
ParsedRoad closestRoad = GetParsedRoadForFactor(t);
764+
t = FactorToRoadFactor(t, closestRoad);
698765

699766
float offset = side == Side.Left ? closestRoad.LeftLaneOffsetsEnd[laneIndex] : closestRoad.RightLaneOffsetsEnd[laneIndex];
700767
float lastOffset = side == Side.Left ? (closestRoad.LeftLaneOffsetsStart != null ? closestRoad.LeftLaneOffsetsStart[laneIndex] : offset)
@@ -1039,6 +1106,16 @@ public Node GetNodeInCommon(ParsedPrefab other)
10391106
throw new ArgumentException("No common node between the two prefabs");
10401107
}
10411108

1109+
public Node GetNodeInCommon(ParsedRoadList roadList)
1110+
{
1111+
foreach (var node in Prefab.Nodes)
1112+
{
1113+
if (node.Uid == roadList.StartNode.Uid || node.Uid == roadList.EndNode.Uid)
1114+
return (Node)node;
1115+
}
1116+
throw new ArgumentException("No common node between the prefab and the road list");
1117+
}
1118+
10421119
public Node GetNodeInCommon(ParsedRoad road)
10431120
{
10441121
foreach (var node in Prefab.Nodes)

ETS2LA.Shared/Shared.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,22 @@ public virtual void Shutdown()
148148
}
149149
}
150150

151+
/// <summary>
152+
/// The base interface for all ETS2LA library plugins.
153+
/// </summary>
154+
public interface ILibraryPlugin
155+
{
156+
PluginInformation Info { get; }
157+
}
158+
159+
/// <summary>
160+
/// The base class for all ETS2LA library plugins. This is just a convenience class that implements the ILibraryPlugin interface.
161+
/// </summary>
162+
public abstract class LibraryPlugin : ILibraryPlugin
163+
{
164+
public abstract PluginInformation Info { get; }
165+
}
166+
151167
/// <summary>
152168
/// Represents the basic information about a plugin.
153169
/// </summary>

ETS2LA.UI/Services/PluginManagerService.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,28 @@ public PluginManagerService()
1515

1616
public List<IPlugin> GetPlugins()
1717
{
18-
if (backend.pluginHandler == null)
18+
if (backend.PluginHandler == null)
1919
return new List<IPlugin>();
2020

21-
return backend.pluginHandler.LoadedPlugins;
21+
return backend.PluginHandler.LoadedPlugins;
2222
}
2323

2424
public void UnloadPlugins()
2525
{
26-
backend.pluginHandler?.UnloadPlugins();
26+
backend.PluginHandler?.UnloadPlugins();
2727
}
2828

2929
public void ReloadPlugins()
3030
{
31-
backend.pluginHandler?.UnloadPlugins();
32-
backend.pluginHandler?.LoadPlugins();
31+
backend.PluginHandler?.UnloadPlugins();
32+
backend.PluginHandler?.LoadPlugins();
3333
}
3434

3535
public bool SetEnabled(IPlugin plugin, bool enable)
3636
{
3737
var ok = enable
38-
? backend.pluginHandler!.EnablePlugin(plugin)
39-
: backend.pluginHandler!.DisablePlugin(plugin);
38+
? backend.PluginHandler!.EnablePlugin(plugin)
39+
: backend.PluginHandler!.DisablePlugin(plugin);
4040

4141
return ok;
4242
}

ETS2LA.UI/Views/ManagerView.axaml.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,15 @@ private void UpdatePluginList()
5858
foreach (var plugin in plugins)
5959
{
6060
Plugins.Add(new PluginItem(plugin, _pluginService));
61-
_pluginService.backend.pluginHandler?.PluginEnabled += (enabledPlugin) =>
61+
_pluginService.backend.PluginHandler?.PluginEnabled += (enabledPlugin) =>
6262
{
6363
if (enabledPlugin == plugin)
6464
{
6565
var item = Plugins.FirstOrDefault(pi => pi.Id == plugin.Info.Id);
6666
item?.Update();
6767
}
6868
};
69-
_pluginService.backend.pluginHandler?.PluginDisabled += (disabledPlugin) =>
69+
_pluginService.backend.PluginHandler?.PluginDisabled += (disabledPlugin) =>
7070
{
7171
if (disabledPlugin == plugin)
7272
{

0 commit comments

Comments
 (0)