Skip to content

Add Router Hook example#7

Draft
robpneu wants to merge 6 commits intosp-tarkov:mainfrom
robpneu:add-router-hook-example
Draft

Add Router Hook example#7
robpneu wants to merge 6 commits intosp-tarkov:mainfrom
robpneu:add-router-hook-example

Conversation

@robpneu
Copy link
Copy Markdown

@robpneu robpneu commented Oct 24, 2025

I noticed there was no equivalent to the old Router Hooks example, https://github.com/sp-tarkov/mod-examples/blob/master/TypeScript/9RouterHooks/src/mod.ts, so this is my attempt at creating one. I expect this will start/continue a conversation from a few days ago in the mods-development channel about the best way to implement router hooks (I am FriedEngineer in discord).

First off, I've written very little C# so I am likely just missing some basic/background knowledge here. Second, this code is minimally functional however a few things feel...weird to me, but I didn't see another way to handle them.

  • returning new ValueTask<string>(output) with the output that the hooked route was sending
    • In my head a hook should only be able to hook onto a route non-invasively (ie not be able to change the output)
  • By "default" the additional RouteAction seems to be blocking the "original" RouteAction
    • I added the HandleRouteAsync example to showcase non-blocking mod code. We could remove the HandleRouteSync method entirely and just trust that people will generally use it, so that the hooked route is not blocked, but I could see some use case where blocking would be desired.

Feel free to rip this code to shreds as well, I won't take it personally. If this helps at all to arrive at the point of having a Router Hooks example in this repo, I will be happy.

@chompDev chompDev requested a review from ArchangelWTF October 24, 2025 10:19
Copy link
Copy Markdown
Member

@ArchangelWTF ArchangelWTF left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some small nits, I would be happy to merge once those are resolved

Comment on lines +38 to +51
private static ISptLogger<RouterHook> _logger;

public RouterHook(
JsonUtil jsonUtil,
ISptLogger<RouterHook> logger) : base(
jsonUtil,
// Add an array of routes to which we want to listen
GetRouteListeners()
)
{
_logger = logger;
}

private static List<RouteAction> GetRouteListeners()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I based this on the the 10CustomRoute example

private static List<RouteAction> GetCustomRoutes()
. Should that be non-static as well? Or is there a difference here that I don't understand?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, ideally a lot of the mod examples were never updated so 10 should receive an update too to match what we do in SPT. But I think starting here would be a good thing

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally aim for single purpose PRs so would you like me to add that in to this PR since they're kinda related or leave for another PR?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love to keep the current PR within it's current scope, I would be open to accepting a new one for updating the other examples that don't follow the standard of what we currently do in SPT however

Comment thread 26RouterHook/RouterHook.cs Outdated
Comment on lines +38 to +45
private static ISptLogger<RouterHook> _logger;

public RouterHook(
JsonUtil jsonUtil,
ISptLogger<RouterHook> logger) : base(
jsonUtil,
// Add an array of routes to which we want to listen
GetRouteListeners()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend doing these in the primary constructor, then they are available and you don't have to make the _logger static to make it available

Copy link
Copy Markdown
Author

@robpneu robpneu Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my other comment, I also based this on another example that used logger, I believe it was the ReadJsonConfig example

public class ReadJsonConfig : IOnLoad // Implement the IOnLoad interface so that this mod can do something
{
private readonly ISptLogger<ReadJsonConfig> _logger;
private readonly ModHelper _modHelper;
public ReadJsonConfig(
ISptLogger<ReadJsonConfig> logger,
ModHelper modHelper)
{
_logger = logger;
_modHelper = modHelper;
}
. Happy to change it, I just don't understand when to choose one method vs the other.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Primary constructor is mostly used throughout SPT and should be used in most of the mod examples too, it's an easier way to use those across the entire class rather than to be forced to assign it in a separate constructor

@robpneu
Copy link
Copy Markdown
Author

robpneu commented Oct 28, 2025

Some small nits, I would be happy to merge once those are resolved

Thanks for the feedback! I did some additional testing and added a few replies. I hope they don't come across as pushing back, I'd just really like to understand the best way to implement this as I'll be using it immediately in updating https://github.com/robpneu/SPT-EventAutoProfileBackup for 4.0

@robpneu robpneu force-pushed the add-router-hook-example branch from 3749c91 to 82c7c2a Compare November 10, 2025 04:19
@robpneu
Copy link
Copy Markdown
Author

robpneu commented Nov 10, 2025

I struggled a bit to update this to use a primary constructor and remove the static, particularly with removing the static. Which of these is closer to what you had in mind?

Separate Callbacks class

Putting the route callback in a separate class and then passing that class into the constructor is the only way I found to not require that the HandleRoute method be static. It's very similar to what is in the example to which you linked previously, https://github.com/sp-tarkov/server-csharp/blob/eaf383bd129eb2c8014130a8cbd1ea52d932804b/Libraries/SPTarkov.Server.Core/Routers/Static/BuildStaticRouter.cs#L12.

public class RouterHook (JsonUtil jsonUtil, RouteCallbacks routeCallbacks)
    : StaticRouter(
        jsonUtil, 
        [
            new RouteAction(
                "/client/game/start",
                async (url, info, sessionId,  output ) => await routeCallbacks.HandleRoute(url, sessionId, output)
            ),
            new RouteAction(
                "/client/game/logout",
                async (url, info, sessionId,  output ) => await routeCallbacks.HandleRoute(url, sessionId, output)
            ),
        ]
    )
{ }
public class RouteCallbacks(ISptLogger<RouterHook> logger)
{ 
    /**
     * Example of a route listener
     */
    public ValueTask<string> HandleRoute(string url, MongoId sessionId, String? output)
    {
        // Your mod's code goes here
        logger.Info($"Hooked route {url} for session {sessionId}");
        Thread.Sleep(5000); // Simulates the mod doing some work (DELETE THIS IN PRODUCTION MODS)

        // Unless you want to modify what the route returned, return the output unmodified.
        return new ValueTask<string>(output);
    }
}

Uses primary constructor but HandleRoute method has to be static

If keeping the callback method(s) in the same class is desirable, I think this is the only option.

public class RouterHook (
    JsonUtil jsonUtil,
    ISptLogger<RouterHook> logger) :
    StaticRouter(
        jsonUtil, 
        [
            new RouteAction(
                "/client/game/start",
                async (url, info, sessionId,  output ) => await HandleRoute(logger, url, sessionId, output)
            ),
            new RouteAction(
                "/client/game/logout",
                async (url, info, sessionId,  output ) => await HandleRoute(logger, url, sessionId, output)
            ),
        ]
    )
{

    /**
     * Example of a route listener
     */
    public static ValueTask<string> HandleRoute(ISptLogger<RouterHook> logger, string url, MongoId sessionId, String? output)
    {
        // Your mod's code goes here
        logger.Info($"Hooked route {url} for session {sessionId}");
        Thread.Sleep(5000); // Simulates the mod doing some work (DELETE THIS IN PRODUCTION MODS)

        // Unless you want to modify what the route returned, return the output unmodified.
        return new ValueTask<string>(output);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants