StrangeLoopGames / EcoModKit

Eco Modkit
https://docs.play.eco
70 stars 26 forks source link

Allow modifying Eco.WebServer.Web.Startup before it gets used in Eco.WebServer.WebServer.StartWebApp #59

Open cemeceme opened 1 year ago

cemeceme commented 1 year ago

Context on the current state Currently, the CostCalculator mod (or more precisely ModUtils) has pre- and post-inject functions around the function Eco.WebServer.Web.Startup.SetupStaticFile(IApplicationBuilder, PhysicalFileProvider) function, in order to add additional entries to the appBuilder. Depending on the order in which a new StaticFileOptions object is added to the appBuilder, it will take precedence or get overridden by other files on the same path (Those added first take precedence).

Describe the solution you'd like Allow modifying or overriding the Eco.Webserver.Web.Startup class before it is used by Eco.WebServer.Webserver.StartWebApp. This way, mods could modify or override the paths that the webserver provides, allowing modifications and additions to the webpage without having to use harmony.

Describe alternatives you've considered Another solution would be to add a way to register additional directories in the Mods folder to act as another path which the webserver would use. This would allow for modders to include html, js, etc. files with their mods that would be accessible through the website. However, this alternative would lack the ability to modify existing files and thus would be incapable of changing any features already provided by the base game, in addition to requiring modifications to the current vue.js implementations to allow adding new sidebar buttons for these additional sites.

What you would caution against I would caution against adding modding support to the WebClient directory directly akin to how it works in the Mods directly. This is because this approach would involve server owners to have to split downloaded mods into two separate directories when adding website mods or require the EcoServer to add logic to do it for them.

This would complicate modding for both modders, as they would need to make sure to structure their mods in some particular way; as well as end users as they would need to understand to use the EcoServer to download mods vs mod.io, or have to understand which mod-provided directories go where.

In contrast, it is currently fairly easy to make single dll or single directory mods that the server owner can simply place in the Mods directory.

JulianMa commented 1 year ago

Right now, you can use an asp.net controller to add routes and also serve static files. The only thing you can't do is overwriting routes, but adding all files of a custom mod directory as routable files should be possible.

So if you add a Controller you could do something like this:


namespace Eco.Plugins.ModFileController
{

    [AllowAnonymous, Route("api/v1/mod")]
    public class ModFileController : Controller
    {

        [HttpGet("static/{$filename}")]
        public FileContentResult Get(string filename)
        {
            byte[] bytes = File.ReadAllBytes(filename);

            return this.File(bytes, "text/html; charset=UTF-8");
        }

    }
}

(this is not tested, but should work in theory, you should also use streams and not read files into ram)

cemeceme commented 1 year ago

That is true, but this feature is specifically to allow for modifying existing routes. I am aware you can provide API endpoints with the 9.6 update, but that doesn't provide a way to add additional functionality to the actual website.

I got requested to post a feature request, by redwyre, on how to make my mod function without the use of harmony. Alternatively a way to add custom components to the javascript would also work. Or even better, proper support to add additional html sites along with sidebar buttons, so that injecting additional code into the /js/app.[...].js file is no longer necessary.

JulianMa commented 1 year ago

Thats true, right now it's a Pain to deal with that. I thought alot about how this can be done the best way. I think the easiest way would be to introduce a new Interface, like IWebPlugin

namespace Eco.Core.Plugins.Interfaces
{
    public interface IWebPlugin : IServerPlugin
    {
        string GetMenuTitle();
    }
}

When the vue app starts, it could query the Server for all Plugins with the IWebPlugin interface and create an entry in the Menu for it, including a route for a custom page for it. The simplest content of this page, would be an IFrame, poining to the location of the Mod inside the eco webserver (like /mod/examplemod/index.html)

For the static files, there could be two options:

  1. Just place them in in the webroot of the ecoserver: - Every mod would have to manage the files by itself to ensure they are up to date - Interface needs another method the determine the mod-location inside the webserver (or its just static, like /mods/<name>/index.html) + Server only needs an api endpoint to list the mods with their title and the path

  2. Introduce a webroot folder, next to the assembly file. + EcoServer would add routes to all files inside, for example: localhost/mods/examplemod/* + EcoServer knows where to point the IFrame to + Easier to manage - More code needs to be written

But maybe there is a better way to do it. This was just the outcome of me brainstorming.

JulianMa commented 1 year ago

quick and dirty PoC with Method 1 that works: https://github.com/JulianMa/Eco/commit/0202c14bfef7a9423064a8f98eb8de0f97407fd2

redwyre commented 1 year ago

I am worried about modifications to the webbin dir getting wiped out by steam validation, plus we can't really support a modified version of the site, so we are considering ways to make this more extensible without having to resort to hacks.

Thanks @JulianMa for the PoC, we'll definitely take that into consideration.

The main issues seem to be:

So an interface like Julian suggested for getting a link in the sidebar is probably likely, perhaps with a function to return static files you wish to mount. If mounting files from embedded resources is desired we can probably handle that too. I would also want to make sure we're mounting everything for a mod under a separate path so mods wont collide.

eg.

// contents of these would be served under something like "/mod/{name}/"
public interface IWebPlugin 
{
    string GetMenuTitle();
    string? GetStaticFilesPath(); // PhysicalFileProvider created with this path
    string? GetEmbeddedResourceNamespace(); // EmbeddedFileProvider used to extract embedded resources that match this
}

public class MyWebPlugin
{
    string GetMenuTitle() => "MySidebarLink";
    string? GetStaticFilesPath() => "www"; 
    string? GetEmbeddedResourceNamespace() => "EmbeddedResource"; 
}

see EmbeddedFileProvider PhysicalFileProvider