Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Init(args) - The practical DI framework

Discussion in 'Assets and Asset Store' started by SisusCo, Dec 21, 2021.

  1. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    The latter issue should also be fixed in the latest version, and I suspect the first one was just a symptom of compilation having failed.
     
  2. hakastein

    hakastein

    Joined:
    Sep 14, 2023
    Posts:
    6
    And I have a question. I have a prefab and an Animator component on one of the children of this prefab. Can I make this Animator a service?

    System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> UnityEngine.UnityException: get_gameObject is not allowed to be called during serialization, call it from Awake or Start instead. Called from MonoBehaviour 'ServiceTag' on game object 'char_test'.
    See "Script Serialization" page in the Unity Manual for further details.
    at (wrapper managed-to-native) UnityEngine.Component.get_gameObject(UnityEngine.Component)
    at Sisus.Init.Service.TryGetGameObject (System.Object client, UnityEngine.GameObject& gameObject) [0x00010] in C:\unity\EVA\Assets\Sisus\Init(args)\Scripts\Services\Service.cs:588
    at Sisus.Init.Service.TryGetFor[TService] (System.Object client, TService& service) [0x00000] in C:\unity\EVA\Assets\Sisus\Init(args)\Scripts\Services\Service.cs:179
    at (wrapper managed-to-native) System.Reflection.RuntimeMethodInfo.InternalInvoke(System.Reflection.RuntimeMethodInfo,object,object[],System.Exception&)
    at System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x0006a] in <7ec8e29954a6455daa48484a381ec418>:0
    --- End of inner exception stack trace ---
    at System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00083] in <7ec8e29954a6455daa48484a381ec418>:0
    at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <7ec8e29954a6455daa48484a381ec418>:0
    at System.Delegate.DynamicInvokeImpl (System.Object[] args) [0x000e7] in <7ec8e29954a6455daa48484a381ec418>:0
    at System.MulticastDelegate.DynamicInvokeImpl (System.Object[] args) [0x00008] in <7ec8e29954a6455daa48484a381ec418>:0
    at System.Delegate.DynamicInvoke (System.Object[] args) [0x00000] in <7ec8e29954a6455daa48484a381ec418>:0
    at Sisus.Init.Internal.ScopedServiceGetter.TryGetFor (System.Object client, System.Object& service) [0x00010] in C:\unity\EVA\Assets\Sisus\Init(args)\Scripts\Services\ScopedServiceGetter.cs:44
    UnityEngine.Debug:LogWarning (object)
    Sisus.Init.Internal.ScopedServiceGetter:TryGetFor (object,object&) (at Assets/Sisus/Init(args)/Scripts/Services/ScopedServiceGetter.cs:50)
    Sisus.Init.ServiceUtility:TryGetFor (object,System.Type,object&,Sisus.Init.Context) (at Assets/Sisus/Init(args)/Scripts/Services/ServiceUtility.cs:197)
    Sisus.Init.ServiceUtility:ExistsFor (object,System.Type) (at Assets/Sisus/Init(args)/Scripts/Services/ServiceUtility.cs:233)
    Sisus.Init.EditorOnly.Internal.InitializerGUI:IsService (System.Type) (at Assets/Sisus/Init(args)/Scripts/Editor/Inspector/InitializerGUI.cs:455)
    Sisus.Init.EditorOnly.Internal.InitializerGUI:UpdateInitArgumentDependentState () (at Assets/Sisus/Init(args)/Scripts/Editor/Inspector/InitializerGUI.cs:224)
    Sisus.Init.EditorOnly.Internal.InitializerGUI:OnInitArgumentServiceChanged () (at Assets/Sisus/Init(args)/Scripts/Editor/Inspector/InitializerGUI.cs:244)
    Sisus.Init.EditorOnly.Internal.ServiceChangedListener`1<CyberCore.Character.Base.PlayerInput>:OnServiceChanged (Sisus.Init.Clients,CyberCore.Character.Base.PlayerInput,CyberCore.Character.Base.PlayerInput) (at Assets/Sisus/Init(args)/Scripts/Editor/Inspector/Editors/ServiceChangedListener.cs:54)
    Sisus.Init.Service:AddFor<CyberCore.Character.Base.PlayerInput> (Sisus.Init.Clients,CyberCore.Character.Base.PlayerInput,UnityEngine.Component) (at Assets/Sisus/Init(args)/Scripts/Services/Service.cs:694)
    System.Delegate:DynamicInvoke (object[])
    Sisus.Init.Internal.ScopedServiceAdder:AddFor (object,Sisus.Init.Clients,UnityEngine.Component) (at Assets/Sisus/Init(args)/Scripts/Services/ScopedServiceAdder.cs:40)
    Sisus.Init.ServiceUtility:AddFor (object,System.Type,Sisus.Init.Clients,UnityEngine.Component) (at Assets/Sisus/Init(args)/Scripts/Services/ServiceUtility.cs:382)
    Sisus.Init.Internal.ServiceTag:UnityEngine.ISerializationCallbackReceiver.OnAfterDeserialize () (at Assets/Sisus/Init(args)/Scripts/Services/ServiceTag.cs:461)
     
  3. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Yes, that would be a great a use case for the Service Tag. You can click the Tag in the Inspector and pick a client availability from the dropdown like
    In Parents
    or
    In Hierarchy Root Children
    (assuming you want it to only be delivered to clients within the same prefab instance).

    It looks from your stack trace like an exception can get thrown by Inspector-related code in the editor code under some conditions. I'll add a tweak to the code that should get rid of this issue.

    If neither
    In Parents
    nor
    In Hierarchy Root Children
    works for your use case (like if you're instantiating the prefab under some parent, instead of the hierarchy root), then you can use the Services component instead, place it to the root of the prefab, and use client availability
    In Children
    .
     
    Last edited: Apr 7, 2024
  4. hakastein

    hakastein

    Joined:
    Sep 14, 2023
    Posts:
    6
    Thx a lot for fix.
     
    SisusCo likes this.
  5. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Hello!
    A few things!

    1. In the ServiceDebugger, I have a lot of these greyed out services. I believe they are just previously destroyed versions of the currently alive ServiceTag, which is the highlighted ones. It isn't super useful for me to see the greyed out ones, unless I am doing some deep debugging. Perhaps show those only in a debug-mode?
    upload_2024-4-25_10-44-50.png

    2. I got an error with initialization using OnDeserialize instead of Awake. The issue there is that the Init wasn't able to find its dependencies, they weren't ready yet. My guess is that they were not ready because they were not yet Instantiated (the dependencies use ServiceTag, and are created at runtime), and that the OnDeserialize is too fast?
    Basically, what I would like is really for the Initialization to happen as soon as possible. Whenever all of the dependencies are ready, then immediately run Init. Is that possible to do? A robust way of making sure your boys always have their dependencies upon being instantiated.
    I can foresee technical difficulties getting it to work, but wanted to put this out there anyway.

    3. Something I thought of before. Sometimes, an Init does not run because a dependency is missing. Say, for example, I forgot to add the `EncounterActionManager` to a prefab, therefore it is not available to `UnitActionManager` which requires `EncounterActionManager` in its Init function. Right now, Init "silently" does not run, and the issue is found only after some error is reported from `UnitActionManager`. Is it possible to get visibility on statuses on Inits?
    For example, if the ServiceDebugger also had another window which showed "pending Init" or "uninitialized MonoBehaviours" which is a list of consumers waiting for their Inits to run, that could be helpful to spot consumers failing to get their dependencies. Perhaps Init can be enforced to run on Awake or OnDeserialize (I figure that is what's happening in 2., right?)

    Thanks as always :)
     
  6. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Here is what the error looks like with OnDeserialize btw

    upload_2024-4-25_13-47-2.png
     
    SisusCo likes this.
  7. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    That's a good idea. I'll add some sort of option for controlling the visibility of destroyed services, as it's often not relevant.

    Logging an error only if all dependencies couldn't be resolved before the object has become active could make sense... then it could be possible for other objects to call the members on the inactive game object before it has been initialized, without the system warning about this - but I feel like it's probably still the best option. I should be able to tweak InactiveInitializer to await until all services are ready, and only initialize the client at that moment. There's an event that gets raised every time that a new service is registered, which I can hook into.

    Yeah at the moment you only get warnings in edit mode and at runtime if you generate and attach an initializer to a client. Otherwise the initialization of clients is treated as optional, so there's no warning if they're never initialized. The rationale for the lack of errors here has been that somebody could in theory only want to inject services in some contexts (e.g. during unit tests), but the clients could also be able to function without them.

    But over time I've been able to figure out ways to incorporate more and more functionality into all clients, that previously used to only be available if an initializer was attached to them. I am currently working on incorporating both runtime exceptions and edit time warnings for all MonoBehaviour<T...> clients, regardless of whether or not they have an Initializer attached. This will be configurable on a per component type basis (not on a per-instance basis, without an initializer), and only available in the editor, to avoid the need introduce any additional state to all components (config will be serialized in the MonoScript's meta data instead).

    Extending the Service Debugger with the ability to display information about clients (especially uninitialized ones) in the loaded scenes is an interesting idea. I'll spend some time mulling over how what form this could take.
     
    theforgot3n1 likes this.
  8. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Sounds interesting! :)

    Having visibility to whats going on and potential errors is great. My previous solution was just all done through FindObjectsOfType.
     
    SisusCo likes this.
  9. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    I have a script that Inits only 1 script called GlobalDebugManager

    upload_2024-5-2_15-15-14.png

    The global debug manager is a service that spawns globally, along a bunch of other global services (exists in DontDestroyOnLoad)

    upload_2024-5-2_15-15-55.png

    However, when both are instantiated into the scene, the CommandCenter remains uninitialized.

    upload_2024-5-2_15-18-1.png


    What can be the reason for this? It only seems to happen with the GlobalDebugManager.

    PS: Currently substituting the Init with an old FindObjectByType call.
     
    Last edited: May 2, 2024
  10. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
  11. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    A free version of Init(args), Init(args) Lite, is now also available in the Asset Store.

    It focuses on just enabling simple pure dependency injection in code, without any of the additional bells and whistles found in the full Init(args) framework, such as automated service injection, initializers, value providers or Inspector tooling.

    It has the following features:
    • Instantiate with upto twelve arguments.
    • AddComponent with upto twelve arguments.
    x != Null
    - easily test if an interface type variable contains a null, unassigned or destroyed reference.

    It can be used to easily make any component unit testable, by making it possible to inject all the objects that it depends on in code in addition to just the Inspector.

    Example component:
    Code (CSharp):
    1. public class TextDisplayer : MonoBehaviour<StringEvent, Text>
    2. {
    3.     [SerializeField] StringEvent stringEvent;
    4.     [SerializeField] Text text;
    5.  
    6.     protected override void Init(StringEvent stringEvent, Text text)
    7.     {
    8.         this.stringEvent = stringEvent;
    9.         this.text = text;
    10.     }
    11.  
    12.     void OnEnable() => stringEvent.AddListener(OnEventRaised);
    13.     void OnDisable() => stringEvent.RemoveListener(OnEventRaised);
    14.     void OnEventRaised(string value) => text.text = value;
    15. }
    Example unit test:
    Code (CSharp):
    1. [SetUp]
    2. public void Setup()
    3. {
    4.     stringEvent = new StringEvent();
    5.     var gameObject = new GameObject();
    6.     text = gameObject.AddComponent<Text>();
    7.     gameObject.AddComponent(out textDisplayer, stringEvent, text);
    8. }
    9.  
    10. [Test]
    11. public void RaisingStringEventUpdatesDisplayedText()
    12. {
    13.     stringEvent.Raise("Test");
    14.     Assert.AreEqual("Test", text.text);
    15. }
     
    theforgot3n1 likes this.
  12. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    SisusCo likes this.
  13. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    There are a couple of MonoBehaviour<GlobalContextManager> on these gameobjects, but they are disabled because it is efficient. I want them to retrieve their dependencies as soon as possible, not when enabled. OnAfterDeseralize causes this error to occur:

    "
    MissingInitArgumentsException: BaseUnitUICriticalDefenseUIView was initialized without being provided with all of the 1 initialization arguments that it requires.
    Make sure that all the Init arguments are registered as services using the [Service] attribute, ServiceTag or Services components, or attach an initializer and configure the services using the Inspector.
    You can also manually pass all the arguments to the client using BaseUnitUICriticalDefenseUIView.Instantiate<GlobalContextManager> or GameObject.AddComponent<BaseUnitUICriticalDefenseUIView, GlobalContextManager.
    "

    Is this something that's possible to do?
     

    Attached Files:

  14. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    The combination of asynchronous initialization and initialization of clients on inactive game objects is not supported out of the gate at the moment, no.

    It would be possible to implement this using a custom initializer, though.

    If you add this base class to your project:
    Code (CSharp):
    1. using System;
    2. using System.Threading.Tasks;
    3. using Sisus.Init;
    4. using UnityEngine;
    5. using Object = UnityEngine.Object;
    6.  
    7. public abstract class InactiveAsyncInitializer<TClient, TService>  : MonoBehaviour, IInitializer<TClient, TService>, ISerializationCallbackReceiver where TClient : Component, IInitializable<TService>
    8. {
    9.     [SerializeField, HideInInspector] private TClient target;
    10.     public Object Target { get => target; set => target = value as TClient; }
    11.     public bool TargetIsAssignableOrConvertibleToType(Type type) => type == typeof(TClient);
    12.     object IInitializer.InitTarget() => target;
    13.     TClient IInitializer<TClient>.InitTarget() => target;
    14.  
    15.     public async Task InitTargetAsync()
    16.     {
    17.        await Task.Yield();
    18.        await Awaitable.MainThreadAsync();
    19.  
    20.        if(!Application.isPlaying || !target)
    21.        {
    22.           return;
    23.        }
    24.  
    25.        TService service;
    26.  
    27.        while(!Service.TryGetFor(target, out service))
    28.        {
    29.           await Awaitable.NextFrameAsync();
    30.      
    31.           if(!target)
    32.           {
    33.              return;
    34.           }
    35.        }
    36.  
    37.        target.Init(service);
    38.     }
    39.  
    40.     async void ISerializationCallbackReceiver.OnAfterDeserialize()
    41.     {
    42.        if(target != null)
    43.        {
    44.           await InitTargetAsync();
    45.        }
    46.     }
    47.  
    48.     void ISerializationCallbackReceiver.OnBeforeSerialize() { }
    49. }
    Then you could create an initializer for ParryStat like this:
    Code (CSharp):
    1. internal sealed class ParryStatInitializer : InactiveAsyncInitializer<ParryStat, GlobalContextManager> { }
    And then attach it to the ParryStat component by pressing the + button in its Init section.


    I'll need to do some thinking about what would be the best way to incorporate built-in support for this into Init(args).

    Right now InactiveInitializer is more limited in many ways than "normal" initializers that can be generated and attached to clients. Probably the optimal solution would be to also add support for generating full-blown Inactive Initializers for clients, with support for everything that normal Initializers can do (asynchronous loading, Inspector drag-and-drop, value providers...).
     
    Last edited: May 18, 2024
    theforgot3n1 likes this.
  15. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    I found a bug with Drawers when using InitArgs.
    I'll upload the video.



    Sometimes, I cannot change enums. It also sometimes happens with strings/ints.
    This doesn't always happen, I restarted my Unity editor and it fixed itself.

    This only happens with MonoBehaviour<SCRIPT>, when I switch to the regular MonoBehaviour it doesn't happen.
     
    Last edited: May 20, 2024
  16. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    I'll investigate this - thanks for letting me know!
     
  17. nathanhalko

    nathanhalko

    Joined:
    Dec 9, 2021
    Posts:
    6
    Hello. Init(args) seems to be broken on Unity 2022.3.29f1. I was able to reproduce this issue by importing the package on a new project. I have a game networked with Mirror that now destroys my GameObjects when I connect a client to a server. This package seems to be the source of the issue.

    Is this version of Unity not supported? Error log below.

    Code (CSharp):
    1. Field Coroutine.routine not found.
    2. UnityEngine.Debug:LogWarning (object)
    3. Sisus.Init.EditorCoroutineRunner:.cctor () (at Assets/Sisus/Init(args)/Scripts/Editor/Coroutines/EditorCoroutineRunner.cs:42)
    4. System.Activator:CreateInstance (System.Type)
    5. Sisus.Init.Internal.EditorServiceInjector:GetInstance (System.Type,System.Type,Sisus.Init.EditorServiceAttribute,UnityEngine.GameObject&) (at Assets/Sisus/Init(args)/Scripts/Services/EditorServiceInjector.cs:288)
    6. Sisus.Init.Internal.EditorServiceInjector:CreateService (System.Collections.Generic.Dictionary`2<System.Type, object>,System.Type,Sisus.Init.EditorServiceAttribute,UnityEngine.GameObject&) (at Assets/Sisus/Init(args)/Scripts/Services/EditorServiceInjector.cs:184)
    7. Sisus.Init.Internal.EditorServiceInjector:CreateServices (System.Collections.Generic.IEnumerable`1<System.ValueTuple`2<System.Type, Sisus.Init.EditorServiceAttribute>>,System.Collections.Generic.Dictionary`2<System.Type, object>,UnityEngine.GameObject&) (at Assets/Sisus/Init(args)/Scripts/Services/EditorServiceInjector.cs:166)
    8. Sisus.Init.Internal.EditorServiceInjector:CreateInstancesOfAllServices () (at Assets/Sisus/Init(args)/Scripts/Services/EditorServiceInjector.cs:136)
    9. Sisus.Init.Internal.EditorServiceInjector:CreateAndInjectServices () (at Assets/Sisus/Init(args)/Scripts/Services/EditorServiceInjector.cs:109)
    10. Sisus.Init.Internal.EditorServiceInjector:.cctor () (at Assets/Sisus/Init(args)/Scripts/Services/EditorServiceInjector.cs:45)
    11. UnityEditor.EditorAssemblies:ProcessInitializeOnLoadAttributes (System.Type[])
     
  18. nathanhalko

    nathanhalko

    Joined:
    Dec 9, 2021
    Posts:
    6
    After more testing, I've confirmed that my broken code was caused by another component.

    As a follow up, how can I suppress or resolve this warning?
     
    SisusCo likes this.
  19. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    @nathanhalko Right yeah, that error will be removed in the next version. Unity removed a field from the Coroutine class which I was using to make it possible to use StopCoroutine(Coroutine) in edit mode during unit tests. Not the biggest loss, especially since StopCoroutine(IEnumerator) and StopAllCoroutines still work.

    I'm kind of in the middle of a little bit larger set of changes currently though, so it could take a few days before I'll be able to submit the next version unfortunately. The warning is harmless though, and can be safely ignored.
     
    nathanhalko likes this.
  20. nathanhalko

    nathanhalko

    Joined:
    Dec 9, 2021
    Posts:
    6
    For anyone using Mirror with Init(args), the two are fully compatible with a slight change to the package.

    Inside of Init's `Scripts/MonoBehaviour/MonoBehaviourT1.cs` script, you will find the following signature
    Code (CSharp):
    1. public abstract class MonoBehaviour<TArgument> : MonoBehaviour, IInitializable<TArgument>, IInitializable
    To get access to Init's features with the NetworkBehaviour class in Mirror, you can duplicate the abstract class and have it inherit from NetworkBehaviour.

    Code (CSharp):
    1. using Mirror;
    2.  
    3. public abstract class NetworkBehaviour<TArgument> : NetworkBehaviour, IInitializable<TArgument>, IInitializable
    Now you get access to syntax like the following

    Code (CSharp):
    1. public class HealthBehaviour : NetworkBehaviour<FloatingTextManager>
    This works because MonoBehaviourT1 and NetworkBehaviour are both extensions of MonoBehaviour and they play nice with each other. Really awesome when code just works like this. I've done this and confirmed it works with my networked game :)
     
  21. nathanhalko

    nathanhalko

    Joined:
    Dec 9, 2021
    Posts:
    6
    I see now that to get NetworkBehaviour<T1, T2..> to function I would have to do this for MonoBehaviourT1-T12. I will probably write a bash script to prepare for this in the next update and will share it here when I do.
     
    SisusCo likes this.
  22. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    @nathanhalko There's also already an add-on for Netcode for GameObjects located at
    Packages\com.sisus.init-args\Add-Ons\Netcode for GameObjects.unitypackage
    , which might be a little bit easier to use as a base.

    With that you'd have to:
    1. replace all instances of
      Unity.Netcode
      with
      Mirror
      in the .cs files, and
    2. change the preprocessor directive NETCODE_FOR_GAMEOBJECTS to MIRROR in the InitArgs.Netcode.asmdef and in all the .cs files (or remove it altogether).
     
    Last edited: May 26, 2024
  23. nathanhalko

    nathanhalko

    Joined:
    Dec 9, 2021
    Posts:
    6
    Thanks for the tip, much easier to do on my end.

    However, I've been working more with my game, and I have a player prefab that gets created which is no longer running Init on clients. It runs on the server okay though. My minimum example works on clients just fine, so its definitely from a change I made along the way. I double checked that I was using OnAwake instead of Awake.

    What would be the likely suspect for this behavior?
     
  24. nathanhalko

    nathanhalko

    Joined:
    Dec 9, 2021
    Posts:
    6
    After looking through the stack trace, I notice that I'm hitting this condition when Init runs on my client objects
    Code (CSharp):
    1. if(!InitArgs.TryGet(context, this, out TFirstArgument firstArgument, out TSecondArgument secondArgument))
    2. {
    3.     return false;
    4. }
     
  25. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    @nathanhalko There's a couple of other potential reasons listed here.

    If all the Init arguments have been registered as services that are accessible to the client at the point that InitArgs.TryGet is called, then it should return true.
     
    Last edited: May 26, 2024
  26. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    I made a quick test with Mirror, and services registered using the [Service] attribute were getting injected to clients in a client build for me.

    Seeing some more information about how your services are registered could help, if the information in the documentation doesn't help.
     
    Last edited: May 26, 2024
  27. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Hello,

    I have been checking out our playmode times and script compilation times.

    Whenever we enter playmode, we have a lot of time (2 seconds in this case) being spent on some EditorOnly sisus stuff. Can this be optimized away somehow? It costs quite a bit.

    upload_2024-5-29_16-13-15.png
     
  28. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,363
    Hey @theforgot3n1 ,

    I believe there's some potential there for optimization. I'll see what I can do.
     
    theforgot3n1 likes this.
  29. theforgot3n1

    theforgot3n1

    Joined:
    Sep 26, 2018
    Posts:
    216
    Awesome :) Looking forward to it!