AetherFramework 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package AetherFramework --version 1.0.0
                    
NuGet\Install-Package AetherFramework -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AetherFramework" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AetherFramework" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="AetherFramework" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AetherFramework --version 1.0.0
                    
#r "nuget: AetherFramework, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AetherFramework@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AetherFramework&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=AetherFramework&version=1.0.0
                    
Install as a Cake Tool

Aether Framework

Welcome to Aether Framework a simple and lightweight modding framework.

The main purpose of this modding framework is to keep it simple to add modding on different type of projects and applications.

The framework itself its pretty modular and it's built on top of that mindset, allowing the developer to be able to change aspects of the framework without having to change a lot from it.

Now let's get to the framework features, shall we?

Features

Class overwriting (da best feature), quick and simple event system, mod loading (must have!), and modding engines.

For now the framework only has one modding engine which is the main target of the mod loader, assemblies (.dlls), but you can add one by yourself!

The mod loaders use a mod registry to save up enabled/disabled mods to keep each mod state saved between instances of your application.

Now that we had a quick rundown of features, let's get the usage of the features!

Usage (Class Overwriting)

Class Overwriting is the biggest feature on this Modding Framework (aside from the Event System I built).

This feature comes from here which uses CPPIA (Haxe propietary).

Anyways, let's get into it!

Loading Overridable classes

Before getting into Mod Loading and the cool stuff, you need to help the Class Registry get the "Overridable Classes".

The way you do that is extremely easy:

ClassRegistry.RegisterOverridableClasses(Assembly.GetExecutingAssembly());

once you do that you're ready to go, but what is the Class Registry without the sweet sweet "Overridable Classes"?

The next section will help you with that!

Marking classes as Overridable

Since C# doesn't have a feature like Haxe Macros, I decided to add an Attribute to mark classes as Overridable

The way you use the Attribute in an empty constructor is pretty simple, here have an example:

[OverridableClass]
public class ExampleClass
{
  // methods
}

pretty easy huh? If you want another example, check this one

If the class you want to override has arguments in the constructor, don't worry! The Attribute can help in that:

[OverridableClass(typeof(string), typeof(int))]
public class ExampleClass
{
  public ExampleClass(string str, int num)
  {
    // do something
  }
}

that's somewhat bloaty but it's necessary to help Reflection when working with class instances from Overridable Classes, if you want anoda example, check this one

It may be missing a lot of functionality, but for now the essentials are working properly (haven't tested if the out or in keywords).

Class Overwritten Event

This event gets fired each time a class on the registry gets overwritten, this can be used to refresh anything that is using the Class Registry, being as dynamic as possible.

Check this example that refreshes the current game screen whenever there is a class overwritten.

How to use them

Now that you know how to fill up your Class Registry with overridable classes, we are going to learn how to use them.

You can overwrite the Class Registry classes everywhere, in a mod, in the base game, anywhere you think of, this makes it extremely dynamic;

Let's see a simple example:

[OverridableClass]
public class HelloWorld
{
  public virtual void DoSomething()
  {
    Console.WriteLine("Hello World!");
  }
}

now, this class should be in the Class Registry already, let's get to do another class that overwrites that behaviour

public class HelloHell : HelloWorld
{
  public override void DoSomething()
  {
    Console.WriteLine("Hello Hell!");
  }
}

once you have a class that overwrites the base class behaviour, let's rewrite it!

ClassRegistry.OverwriteClass<HelloWorld, HelloHell>();

that's easy! But how do I create an instance of it, that's easy too:

HelloWorld instance = ClassRegistry.CreateInstance<HelloWorld>();

now, calling DoSomething should return Hello Hell!, let's check it out:

instance.DoSomething(); // "Hello Hell!"

that's amazing and easy isn't it?

Caveats

Overwriting an already overwritten class can have weird behaviour, you can take a look at Mod Example 2.

This approach makes it that the Overridable Class needs to have a lot of methods in order to make it extremely moddable.

You're ready to go!

Once you have the Class Registry filled up with your cool classes, we need to get with the next topic, mod loading!

Usage (Mod Loader)

The usage is really simple, all you have to do is create a ModLoader which handles everything by itself (The mod loading and assemblies, registries, etc)

You can pass a couple of arguments to customize your mod loader, lets get to them

  • folder basically the folder INSIDE the output directory where the mod loader will scan for mods.
  • filePrefix a quick filter for which files to load, if you only want to load for example Indexer.Format.<rest> you would pass Indexer.Format.
  • engine the modding engine the mod loader is gonna use, defaults to Assembly Engine, the modding engine will handle the folder and filePrefix arguments properly.
  • config the configuration provider used by the ModRegistry to save some needed configuration for the mod loaders, usually a list of enabled/disabled mods.

Once you create a new Mod Loader, it will automatically apply Hot Reload events to the event manager.

You can see an example of some of those arguments being changed in here.

Warning

ALWAYS register overridable classes BEFORE creating a mod loader.

Fun Fact

RegisterOverridableClasses is also executed whenever it loads a mod (with the Assembly Engine)

Maybe you can make cool stuff inside mods when overriding mod classes? I haven't really tried that but it would be crazy amazing ngl

Ready to go!

Once you have all of this, you're all set, the essentials are done, you might want to make the mod loader a singleton IF you're not thinking of adding more modding engines to your application (for example a custom Wren engine)

· But what about events? You mentioned it before.

Yeah that's the next topic

Usage (Event Manager)

The EventManager is another great feature of this Modding Framework and probably the only thread safe thing in the whole framework.

This event system handles 2 types of events, global events and targeted events (based on Intents most of the time, to know how to use them, go to the IMod usage section)

Fun Fact

When creating a new modding engine, the Mod Registry inside of it, will get automatically added to the Event Manager.

Registering a handler for global events

It's as easy as just doing

EventManager.Register<EventType>(EventRegistryType.GLOBAL, handler);

private void handler(EventType ev)
{
  // do something
}

you can see an example of registering a handler for the HRUpdateApplicationEvent (Hot Reload Application Event) here.

You can register a global handler anywhere in your application, it's not only for Mods, but Targeted Events are, so let's get to them!

Registering a targeted event handler (Intents)

This one gets only a little bit more harder, but not that hard, it's only adding an intent and nothing else!

The Intents will get detailed on the IMod usage section, for now it's only to show how to listen for a targeted event.

We will use an existing example for this section, ScreenFadeMod from ModExample2, TestEvent from MainScreen and GameIntents.SCREEN_LOAD_COMPLETE from GameIntents (refer to the Intents section of IMod for recommendations)

Lets start with the mod creation

public class ScreenFadeMod : IMod
{
  public ModManifest Manifest => new()
  {
    // your manifest fields
    Intents = ["SCREEN_LOAD_COMPLETE"]
  };

  // interface implementation
}

The intents provided inside the manifest will get used to be able to fire events targeted to THAT intent, once that is done we can go and listen to events targeted to that type of event!

public void OnEnable()
{
  // Registers a targeted event listener for TestEvent from THIS mod
  EventManager.Register<TestEvent>(EventRegistryType.TARGETED, handler, this);
}

And voilà you have a targeted intent event, to fire it its as easy as the following:

EventManager.TriggerEventByIntent(new TestEvent(this), "SCREEN_LOAD_COMPLETE");

it's not really targeted when you think of only dispatching the events to a specific mod but since you cannot specifically access the Mods (with its typing but can only access the interface) the safest and quickest way to dispatch specific events is this one.

Registering a targeted event handler (IMod)

This is the real targeted events, for this you need a saved instance of the mod you want to target and an event that extends TargetedEvent

In the mod handler you just need to do the previous steps:

EventManager.Register<EventType>(EventRegistryType.TARGETED, handler, this);

And in the triggering is easier

IMod mod = ...targetMod;
// Create a new instance of your event that extends TargetedEvent and pass the target mod to it
EventManager.TriggerTargetedEvent(new EventType(mod, ...));

This makes it extra specific to a single mod but that's the real intention of the targeted event handlers.

Built-in Events

There are only 2 built-in events on the framework to avoid the bloat as much as possible, the 2 built-in events are related to the hot reload:

these 2 events pass the types that changed since the hot reload, you can see an example of manually updating overwritten classes whenever there is an update application event that passes the mod types here

Event Behaviour

The event behaviour can be modified by custom events, it also includes an Adjust function to be able to change any parameter of the event, this can be useful for moments where the mod can change the default values of an event and the game will use the values of the event for something, have an example:

public class DamageEvent : Event
{
  public int DamageAmount { get; private set; }
    
  public DamageEvent(int damageAmount)
  {
    DamageAmount = damageAmount;
  }

  public override void Adjust()
  {
    // Adjust the damage amount, for example by applying a global multiplier
    DamageAmount = (int)(DamageAmount * 0.9); // Apply a 10% reduction
  }
}

// To see how the IMod interface works, look at the IMod usage section
public class DamageReductionMod : IMod
{
  // implement interface...
  public void OnEnable()
  {
    EventManager.Register<DamageEvent>(EventRegistryType.GLOBAL, OnDamageEvent);
  }

  private void OnDamageEvent(DamageEvent e)
  {
    if (!e.IsCancelled) // Check if the event has been cancelled by another handler
    {
      e.Adjust();  // Apply any global adjustments first
      e.DamageAmount = (int)(e.DamageAmount * 0.5); // Further reduce damage by 50%
    }
  }
}

// Example of triggering a DamageEvent
var damageEvent = new DamageEvent(100); // Initial damage is 100
EventManager.TriggerGlobalEvent(damageEvent);

Console.WriteLine($"Final Damage: {damageEvent.DamageAmount}");

Event Cancelling

When cancelling an event, it will stop propagating to the next handlers, you can see the behaviour here

IMod Usage

Now that you learnt the Modding Framework essentials, let's get to modding shall we?

Only by implementing the IMod interface the mod will get automatically loaded into the framework, so let's get into the fields.

Manifest

Refers to ModManifest for the Mods Manifest (duh) for the framework, it contains the essentials for a simple manifest object:

  • Name: the name of the mod
  • Description: the description of the mod (will most likely get used if there is something to list all the mods loaded).
  • Author: the author of the mod (can be used with the name: ModName by Author).
  • Intents: a list of intents, will get explained in the next section.
  • Version: a version object that can be used for an auto updater or something.

Intents

automatically redirects to ModManifest.Intents

This is used to let the framework know which parts of the application is going to access OR to listen to events that target that intent.

You can also use them in a different way in your application for increased security to parts of the code.

Recommendation

I recommend having a class like GameIntents to ensure safety between intents.

On Enable

This function will get executed once the mod gets enabled, it's recommended to overwrite any class the mod is going to use or register event handlers in here.

On Disable

This function will get executed once the mod gets disabled, it's recommended to make any cleanups here, related to overwritten classes, registered event handlers, etc...

Regarding the README

All of the usage documentation will be moved on to the Wiki of the repository, all the readme comes from the original repo the framework was in, once I have time and the new example running, I'll work on moving the stuff to the Wiki, for now it's gonna stay here.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 165 10/11/2024
1.0.0 163 8/28/2024

First public release of Aether Framework.