roflmuffin / CounterStrikeSharp

CounterStrikeSharp allows you to write server plugins in C# for Counter-Strike 2/Source2/CS2
https://docs.cssharp.dev
Other
795 stars 125 forks source link

The whole design of CounterStrikeSharp #71

Closed laper32 closed 8 months ago

laper32 commented 11 months ago

Instead of shared data between app domains, I'd better call it: The whole design of CounterStrikeSharp.

You could find that shared data between app domains is the subset of the whole design of 'CounterStrikeSharp'.

Why we discuss it? Well, this project, CounterStrikeSharp, will be renamed to a better name in the future, and it will directly affect the future of the project, and the future of the whole ecosystem.

Any progress will be updated in this issue.

For now, yes, we are now using C# for scripting, but if I remember correctly, one of the aims is: any language what supports the format can be integrated easily and can also share data between different languages.

This requires us to make well-designed architecture. Generally, this MUST lead to a result: We are doing an engine, a tiny Operating System, in fact.

If we want to do it, Unreal, Unity, etc. must be important references, and also S&Box, which is the only game what using C# for scripting in Source 2.

Now, back to our question: How many layers should we have?

  1. Engine layer. It can also be called Unmanaged layer (aka, not managed by the next layer), which handles such things:
    • Interact with native engine.
    • Provides infrastructures, which may include:
      • Reverse-engineering tools
      • Centralized logging
      • Hacked functions
      • Game-specific information
    • Distributed folder layout It should also be easily expanded.
  2. Runtime layer. It handles such things:
    • Provide user space infrastructures, may include:
      • GC
      • User friendly API
      • Simplified error handling
      • Distributed folder layout It inherits the engine layer, and adds user space specific layers. It won't contradict the engine layer folder layout.
    • Wrapper of engine layer APIs
    • Application infrastructures. Based on the above, in general, we would select something VM-based, like Android of Java, Unity of C#, Unreal of Blueprint. Also, this layer should also be expanded easily. Our choice is C#.
  3. Application layer. Since previous layers have already provided all infrastructure what you need, you now can write your own application. These applications should be restricted in their own space, and MUST NOT let them pollute any other, even any parent layers.

So, we have discussed a lot, what should it look like? For distributed folder layout:

. // addons/counterstrikesharp
|
|---bin                                                1)
|---config                                             2)
|---log                                                3)
|    |---L20231101.log
|    \---L20231102.log
|---gamedata                                           4)
|    |---counterstrikesharp.games.kv
|    \---cs2.games.kv
|---modules                                            5)
|    |---managed                                     5.a)
|    \---unmnaged                                    5.b)  
\---plugins                                            6)
     |---disabled                                      7)
     |---Warcraft                                      8)
     |    |---bin                                      9)
     |    |    |---Warcraft.API.dll
     |    |    \---Warcraft.Core.dll
     |    |---config                                  10)
     |    |---gamedata                                11)
     |    |    |---warcraft.games.kv
     |    |    |---whatever.games.kv
     |    \---Plugin.toml                             12)
     |---Admin
     |---VIP
     |---ZombiePlague                                 13)
     |    |---ZombiePlague.Core
     |    |---ZombiePlague.Weapons
     |    \---ZombiePlague.Classes
     \---Store

where:

  1. Core binaries, which contain engine layer output and runtime layer output, and all of these extensions, and modules.

    For example, for now, unmanaged output is counterstrikesharp.dll, the runtime is CounterStrikeSharp.API.dll

  2. Core configs. Other layers can only read, but can not write.
  3. All log messages should be written in one place.
  4. Core gamedata. Other layers can only read, but can not write.
  5. Provides additional modules for engine layer and runtime layer. a. Unmanaged, what engine layer uses b. Managed, what runtime layer uses.
  6. Plugins folder. All plugins are based on folders.
  7. If you don't want to modify the config, just simply drag the plugin in what you don't want.
  8. Plugin folder
  9. Plugin binaries
  10. Plugin config
  11. Plugin game data
  12. Plugin configuration, which declares the plugin information
  13. A folder which can contain multiple plugins, since we control the plugin node by looking for Plugin.toml

    Noting that a plugin can have no binaries, just only config or gamedata provided, or some plugins provides a new language, then this plugin's sub-plugins, definitely, is the ecosystem of the language.

For implementation, in my opinion, we should:

Thus, what can we do?

We use the current version of css as an example.

The current version of css's layer is:

Currently, for the managed side, we have only CounterStrikeSharp.API.dll, which contains all implementations and interfaces.

Indeed, it is a single file, easy to develop, and easy to take. People will only take care of the API.

But what will happen if css updated? Well, it may crash, right? Then that's the problem.

So we need to separate the implementation and interface, I will call them:

Then, no matter how implementation changed, it won't affect the interface. The Plugin will only invoke the API, and will not take care of how this interface is implemented.

Also, the CounterStrikeSharp.Core.dll also acts as a runtime of application and unmanaged side.

On the unmanaged side, as I stated previously, it interacts with the game engine, provide reversed-engineering stuffs, etc., and this is exactly what is doing.

OK, you may say: "This is too abstract, I want to see the sample code!", now here we go. For Core, as I said, interacts with engine layer and interface layer, it is used to initialize the scripting runtime.

// Example core, part of the CounterStrikeSharp.Core.dll

class Bootstrap
{
    [Unmanaged]
    public static bool Initialize()
    {
        // These are all samples, what we need to initialize are all depends on
        // the implementation in the future.

        // Initialize sharesys
        // Initialize configsys
        // Initialize additional systems
        // Initialize pluginsys
        // Initialize modulesys
        // Load all modules inside the plugin
    }

    [Unmanaged]
    public static void Shutdown()
    {
        // unload all of them, reversed sequence.
    }
}

the CounterStrikeSharp.API.dll, provides the API exposed from .Core, and also wraps APIs from engine layers.

KillStr3aK commented 11 months ago

and also S&Box, which is the only game what using C# for scripting.

FiveM (the native system is based on theirs)

You need to understand that the McMaster plugin loader works like this: image image

And since each PluginContext has their own AppDomain (just like in FiveM, atleast in mono-v1, not sure about the newer one) we can't really access something from another AppDomain without unwrapping the assembly again which is basically leads us to nowhere.

I'd either suggest how the exports are implemented in FiveM, or dependency injection in the HOST so other plugins will be able to add their services, and dependant plugins get them using the provider from the host. (This idea is pretty skeptical as I'm not sure if that DI could even work as a shared type in the McMaster could work: https://github.com/SourceSharp/runtime/blob/master/Core/PluginManager.cs)

About splitting Core and API, I'm not sure why would it be needed in general. The way DI would work for normal interfaces are okay, but what about natives? I don't think there is any point on this.

We can embed other languages easily because of the native scripting system that is currently available as it has the unmanaged part done and can be generated to any language mostly (with some prework)

But what will happen if css updated? Well, it may crash, right? Then that's the problem.

Plugins can set what version of css they require to run to avoid crashes

roflmuffin commented 11 months ago

I think dependency injection in the host is a good idea, I would like to also add a bridge interface that allows plugins to register their own services to a shared container. The biggest problem is hot reloading in such scenario when most DI containers would not handle dynamic re-loading of the application container

laper32 commented 11 months ago

Perhaps we need a definition of: What is native? By experience, it follows:

Now, looking into the definition of native, current implementation, and our experience, we can find a fun fact that: For detour/reverse engineering part, managed side are all using functions/infrastructures from unmanaged side, no matter what it is (Detoured function/event, Hook tools, etc).

So, based on it, we can make a derived result: No matter how managed side changed, the native inside unmanaged side will not be affected.

Also,

Additionally:

Why I prefer separate API and core? Well, in my opinion, it could:

laper32 commented 11 months ago

For the plugin system, I've noticed a problem that: binary files, config files, data files(incl. SQL .db, some plugins generated cache, etc) are completely messed up. Well, despite these can be user-defined, but in general, user won't. Mostly, they will keep everything in a single folder if you don't pre classify.

In fact, this comes out a greater problem: What is plugin system?

From wikipedia, the definition is:

In computing, a plug-in (or plugin, add-in, addin, add-on, or addon) is a software component that adds a specific feature to an existing computer program. When a program supports plug-ins, it enables customization.

A theme or skin is a preset package containing additional or changed graphical appearance details, achieved by the use of a graphical user interface (GUI) that can be applied to specific software and websites to suit the purpose, topic, or tastes of different users to customize the look and feel of a piece of computer software or an operating system front-end GUI (and window managers).

With a single phrase, it could be: Provide a method to extend the software itself. Different people will have different implementation. Game engine like Unreal (what I'm familiar with), the plugin can be:

Why a plugin provides so many features? If we focusing on game industry, we can find that a game engine should find a way to adapt as many game type as possible. If we jump out of game industry, we could find that different people have different demands, some of them even contradict.

And remember that Unreal's plugin system has been developed many years, we are now just beginning.

In fact, we have already implemented some of these features (e.g.: Spearate all plugins into their own directories), this is a good beginning, we just need to iterate it.

Now, we have been discussed too much. What should we do in the future? Better step-by-step.

However, we still need to provide a method to declare plugin infromation, which are all in Plugin.toml

I'm prefering use toml as configuration file, but this can be changed as any config type.

The config file identifies the plugin name, author, description, version, modules need to load, etc.

Noting that for all plugins, several directories need to be reserved. Plugin cannot override its usage. These directories are:

For example, someone provided a new language, but provided as a plugin. It marks scripts/this_language as the scan directory. But no matter how the directory defines, nobody can define bin as their own specific usage directory, even if config and gamedata.

The plugin can also have dependencies, which is common for sub-plugins.

Also, plugin can be disabled by modify Plugin.toml's field what we provided, or just simply drag it into disabled.

laper32 commented 11 months ago

For logging, we should do such things:

  1. Message log to file
  2. Display them

However, during the experience of sourcemod, we will meet a problem when you are running something with multithread:

  1. How to ensure message logged is sequential?
  2. We also need to log information of unmanaged side, this will really help if some update from Valve fucked up something, which helps us to track what happened. (At least for now, further discussion required)

Based on above, that's why I will put logging into engine layer, which is really necessary.

Now, we have spdlog, it looks like we just need to write a wrapper, then that's all. (Perhaps? review required)

The logging format? Generally, it will be [Timestamp:yyyy/mm/dd HH:MM:SS] [LogLevel] [Logger] [FullFunctionSymbol] Message

Then you will find that the logger name is the specific space of its name.

The plugin will be: [Timestamp:yyyy/mm/dd HH:MM:SS] [LogLevel] [PluginName] [FullFunctionSymbol] Message

Example:

[2023/11/14 10:41:35] [Error] [Warcraft] [Warcraft::WarcraftPlugin::Load(154)] Something fucked up when starting the game!
Exception: Exception thrown when handling blabalbla...
  at blablabla.blablabla
  ...
  (InvocationChainEnd)

The managed side: [Timestamp:yyyy/mm/dd HH:MM:SS] [LogLevel] [Runtime] [FullFunctionSymbol] Message

Example:

[2023/11/14 10:41:35] [Critical] [Runtime] [CounterStrikeSharp::API::Helpers::LoadAllPlugins(75)] Fatal error!
Exception: NativeInvoke does not found!
  at blablabla.blablabla
  ...
  (InvocationChainEnd)

The unmanaged side: [Timestamp:yyyy/mm/dd HH:MM:SS] [LogLevel] [Core] [FullFunctionSymbol] Message

Example:

[2023/11/14 10:41:35] [Info] [Core] [counterstrikesharp::DotNetManager::Initialize(45)] Loading fxr...
KillStr3aK commented 11 months ago

[Timestamp:yyyy/mm/dd HH:MM:SS] [LogLevel] [FullFunctionSymbol] [PluginName] Message

with huge respect towards the cooking master (you laper32) that format gives me ancient java vibes with anxiety, but I like the NativeInvoke related exception handling

laper32 commented 11 months ago

[Timestamp:yyyy/mm/dd HH:MM:SS] [LogLevel] [FullFunctionSymbol] [PluginName] Message

with huge respect towards the cooking master (you laper32) that format gives me ancient java vibes with anxiety, but I like the NativeInvoke related exception handling

windows support delayed half a day in fact since NativeInvoke fucked up because of extern rule is different between lin and win, and also because of exception message does not log fully at that time lol.

laper32 commented 10 months ago

About error handling:

No matter how many layers do we have in a big project, the error handling is still a headache problem. It need to:

Accelerator and FiveM is using google's breakpad to save the problem what I've stated above.

But, we still need to:

If these problem solved, then everything will be happy.

laper32 commented 10 months ago

Also, the growing speed is incridible fast, I think it's time to think a better name to rename CounterStrikeSharp.

johnoclockdk commented 10 months ago

Also, the growing speed is incridible fast, I think it's time to think a better name to rename CounterStrikeSharp.

CounterStrikeSharp is fine