Facepunch / sbox-issues

177 stars 12 forks source link

Allow `Tasks` to Be Scheduded During Particular `GameObjectSystem.Stages` and Ran Manually by Particular `GameObjectSystems` #6372

Open LeQuackers opened 2 months ago

LeQuackers commented 2 months ago

For?

S&Box

What can't you do?

I'm unable to configure a given Task to run during a particular Stage or be ran manually in a particular GameObjectSystem, which isn't ideal for Tasks that need to run during certain times.

How would you like it to work?

  1. A GameTask.RunDuringStage(Func<Task>, GameObjectSystem.Stage, CancellationToken) static method, which forces Tasks to be ran/scheduled during a particular Stage:
    
    // Some task that needs to ran during the physics step:
    GameTask.RunDuringStage(SomeTaskAsync, Stage.PhysicsStep, token);

// Lerp the given GameObject's color after fixed update is finished with a random delay between them: GameTask.RunDuringStage(() => LerpColorAsync(gameObject, Color.Blue, delay: new RangedFloat(0.25f, 2.0f)), Stage.FinishFixedUpdate, token);


2. A `TaskWalker` class ([fiddle proof of concept](https://dotnetfiddle.net/H4SH8v)) that devs can instantiate, which would allow them to manually control when queued `Tasks` are ran/stopped:
```csharp
public class MyGameObjectSystem : GameObjectSystem
{
    // This will be set elsewhere. If false, registered tasks shouldn't be ran at all.
    public bool ShouldRunTasks { get; set; } = false;

    public TaskWalker TaskWalker { get; } = new();

    public MyGameObjectSystem(Scene scene) : base(scene)
    {
        Listen(Stage.UpdateBones, -1, DoThing, "MyGameObjectSystem_Worker");
    }

    private void DoThing()
    {
        if (ShouldRunTasks)
        {
            TaskWalker.DoStep();
        }
    }
}

...

// Somewhere in a component:
var walker = Scene.GetSystem<MyGameObjectSystem>().TaskWalker;
walker.StartNew(DoThingBeforeUpdateBones);
walker.StartNew(() => DoThingBeforeUpdateBonesWithArguments(this, 1, "string"));

...

// Trigger the tasks to run/stop somewhere in the code:
var system = Scene.GetSystem<MyGameObjectSystem>();
system.ShouldRunTasks = true;

What have you tried?

Using my fiddle proof of concept in S&Box, but almost none of it is whitelisted:

Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.Task.get_IsFaulted()
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.TaskScheduler..ctor()
Whitelist Error: System.Collections.Concurrent/System.Collections.Concurrent.ConcurrentStack`1.TryPopRange( T[] )
Whitelist Error: System.Collections.Concurrent/System.Collections.Concurrent.ConcurrentStack`1..ctor()
Whitelist Error: System.Collections.Concurrent/System.Collections.Concurrent.ConcurrentStack`1.get_Count()
Whitelist Error: System.Collections.Concurrent/System.Collections.Concurrent.ConcurrentStack`1
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.Task.get_Factory()
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.TaskScheduler
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.Task.get_IsCanceled()
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.TaskFactory
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.TaskScheduler.TryExecuteTask( System.Threading.Tasks.Task )
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.TaskFactory.StartNew<TResult>( System.Func`1<TResult>, System.Threading.CancellationToken, System.Threading.Tasks.TaskCreationOptions, System.Threading.Tasks.TaskScheduler )
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.TaskExtensions.Unwrap( System.Threading.Tasks.Task`1<System.Threading.Tasks.Task> )
Whitelist Error: System.Private.CoreLib/System.Threading.Tasks.TaskCreationOptions
Whitelist Error: System.Collections.Concurrent/System.Collections.Concurrent.ConcurrentStack`1.Push( T )

Additional context

No response

garrynewman commented 2 months ago

I have no idea what you're trying to do here

LeQuackers commented 2 months ago

I'm trying to get a given Task to be scheduled during a particular Stage (in my case just before the physics step).

The fiddle proof of concept shows a working example of how to "step" through a Task, i.e. you could use it in a GameObjectSystem.Listen callback and force some group of Tasks to be scheduled during whatever Stage you need.

Being able to "step" through Tasks using the fiddle proof of concept is a nice because you would have fine control of when Tasks are scheduled without needing to modify each Task's code.

The GameTask.RunDuringStage method was an alternative idea if the fiddle proof of concept wasn't allowed for whatever reason.

Example Code

using System;
using System.Threading.Tasks;

public sealed class ExampleComponent : Component
{
    protected override void OnAwake()
    {
        _ = SomeExampleTaskAsync();
    }

    private async ValueTask SomeExampleTaskAsync()
    {
        while ( true )
        {
            await GameTask.DelayRealtime( 100 );
            Log.Info( $"This task is running during the {StageListener.CurrentStage} stage." );
        }
    }
}

public class StageListener : GameObjectSystem
{
    public static Stage CurrentStage { get; private set; }

    public StageListener( Scene scene ) : base( scene )
    {
        foreach ( var stage in Enum.GetValues<Stage>() )
        {
            Listen( stage, -1, () => CurrentStage = stage, Enum.GetName( stage ) );
        }
    }
}

Output

image

Currently ExampleComponent.SomeExampleTaskAsync is always running (being scheduled) after Stage.FinishUpdate, which is at the very end. I want force it to run (be scheduled) during a particular Stage (in my case just before the physics step).

LeQuackers commented 2 months ago

Something like await GameTask.WaitForStage(GameObjectSystem.Stages stage, int order, CancellationToken token = default); would probably also work.

garrynewman commented 2 months ago

So you just want to be able to put async functions in listen?

LeQuackers commented 2 months ago

So you just want to be able to put async functions in listen?

I want to make arbitrary Tasks be ran/scheduled during a particular Stage.

The fiddle example was an example of how to get that working. Because the TaskWalker class allowed you to manually "step" through a task, you could literally force a given Task or group of Tasks to run using TaskWalker.DoStep.

I mentioned GameObjectSystem.Listen because the listener callback will run during whatever Stage you specify, so if I call TaskWalker.DoStep in the listener callback, then every Task queued in that TaskWalker will be literally forced to run during that Stage.

Example of What I Want:

I want the ExampleComponent.SomeExampleTaskAsync method from the example code a few comments above to run just before the physics step.