diff --git a/ModernUO.sln b/ModernUO.sln
index 779005fbe..de59750f0 100644
--- a/ModernUO.sln
+++ b/ModernUO.sln
@@ -12,6 +12,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UOContent.Tests", "Projects
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Projects\Application\Application.csproj", "{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AddonExample", "Projects\AddonExample\AddonExample.csproj", "{73F435F7-7D30-4275-8B74-D80A0BD7AC23}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Analyze|Any CPU = Analyze|Any CPU
@@ -49,6 +51,12 @@ Global
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E9849FA1-D4F5-4D68-A36B-249F4CB4E374}.Release|Any CPU.Build.0 = Release|Any CPU
+ {73F435F7-7D30-4275-8B74-D80A0BD7AC23}.Analyze|Any CPU.ActiveCfg = Analyze|Any CPU
+ {73F435F7-7D30-4275-8B74-D80A0BD7AC23}.Analyze|Any CPU.Build.0 = Analyze|Any CPU
+ {73F435F7-7D30-4275-8B74-D80A0BD7AC23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {73F435F7-7D30-4275-8B74-D80A0BD7AC23}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {73F435F7-7D30-4275-8B74-D80A0BD7AC23}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {73F435F7-7D30-4275-8B74-D80A0BD7AC23}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Projects/AddonExample/001-new-project.png b/Projects/AddonExample/001-new-project.png
new file mode 100644
index 000000000..3e1e39b98
Binary files /dev/null and b/Projects/AddonExample/001-new-project.png differ
diff --git a/Projects/AddonExample/Addon.cs b/Projects/AddonExample/Addon.cs
new file mode 100644
index 000000000..78718cac2
--- /dev/null
+++ b/Projects/AddonExample/Addon.cs
@@ -0,0 +1,32 @@
+using Server.Accounting;
+
+namespace Server.Addon;
+
+public class Addon
+{
+ public static void Configure()
+ {
+ // ModernUO lifecycle hook before world loads
+ EventSink.AccountLogin += AccountLogin;
+ }
+
+ public static void Initialize()
+ {
+ // ModernUO lifecycle hook after world loads
+ }
+
+ public static void AccountLogin(AccountLoginEventArgs e)
+ {
+ if (Accounts.GetAccount(e.Username) is not Account account)
+ {
+ return;
+ }
+
+ if (!account.Young)
+ {
+ return;
+ }
+
+ account.RemoveYoungStatus(0);
+ }
+}
diff --git a/Projects/AddonExample/AddonExample.csproj b/Projects/AddonExample/AddonExample.csproj
new file mode 100644
index 000000000..ec94c0387
--- /dev/null
+++ b/Projects/AddonExample/AddonExample.csproj
@@ -0,0 +1,22 @@
+
+
+ Server.Addon
+ AddonExample
+ ModernUO Example Addon
+ ..\..\Distribution\Assemblies
+ ..\..\Distribution\Assemblies
+ Debug;Release;Analyze
+
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
diff --git a/Projects/AddonExample/Migrations/keep b/Projects/AddonExample/Migrations/keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/Projects/AddonExample/README.md b/Projects/AddonExample/README.md
new file mode 100644
index 000000000..5996fdcde
--- /dev/null
+++ b/Projects/AddonExample/README.md
@@ -0,0 +1,173 @@
+# Addon Assembly Tutorial
+
+### Create a Project
+
+In your IDE, create a new project in the ModernUO solution. For this example we'll call it `AddonExample`. It's important that the project is created as a subdirectory of the `Projects` directory within the repository.
+
+
+
+### Configure that Project
+
+The process differs between IDEs, but we want to navigate to and open the `AddonExample.csproj` file. Replace the contents of the file with the following contents, and we will walk through it step by step.
+
+```xml
+
+
+ Server.Addon
+ AddonExample
+ ModernUO Example Addon
+ ..\..\Distribution\Assemblies
+ ..\..\Distribution\Assemblies
+ Debug;Release;Analyze
+
+
+
+ false
+
+
+ false
+
+
+
+
+
+
+
+```
+#### `PropertyGroup`
+
+* `RootNamespace` is the namespace of our addon and we will use this when we write the code.
+* `AssemblyName` will determine the name of the DLL file that gets built. This is important for configuration later.
+* `Product` is an arbitrary name to identify your addon.
+* `OutDir` and `PublishDir` shouldn't be changed, the `UOContent.dll` file, for example, is output here, and this is where ModernUO will look for our addon.
+* `Configurations` are build configurations, and probably should not change.
+
+#### `ItemGroup` (1)
+
+* `ProjectReference` indicates that we are referencing another project, so that we can reference code in that project in our addon. We do this twice, once for `Server` and once for `UOContent`.
+
+#### `ItemGroup` (2)
+
+* `AdditionalFiles` indicates we want to copy some files into the build environment. If we were to add custom items which are saved and loaded from the game we would need to provide migrations for changes in what those items save.
+
+### Create Migrations Directory
+
+Create the directory `Migrations` within the `AddonExample` project. Add an empty file called `keep` with no extension. This isn't strictly necessary, but good practice.
+
+### Write Some Code
+
+The project should have a directory structure something like this at this point.
+
+```
+ModernUO
+ Projects
+ AddonExample
+ Class1.cs
+ Migrations
+ keep
+```
+
+Remove `Class1.cs` and create a new class called `Addon`. Replace the code in that generated class with the following contents, and we'll walk through it step by step.
+
+```csharp
+using Server.Accounting;
+
+namespace Server.Addon;
+
+public class Addon
+{
+ public static void Configure()
+ {
+ EventSink.AccountLogin += AccountLogin;
+ }
+
+ public static void Initialize()
+ {
+ }
+
+ public static void AccountLogin(AccountLoginEventArgs e)
+ {
+ if (Accounts.GetAccount(e.Username) is not Account account)
+ {
+ return;
+ }
+
+ if (!account.Young)
+ {
+ return;
+ }
+
+ account.RemoveYoungStatus(0);
+ }
+}
+```
+
+#### The Correct Namespace
+```csharp
+using Server.Accounting; // ignore this for now, we will use it below
+
+// this is the RootNamespace from AddonExample.csproj
+namespace Server.Addon;
+```
+
+#### The Configure and Initialize Functions
+
+```csharp
+ public static void Configure()
+ {
+ /*
+ The Configure function is called once before the World is loaded.
+ Do setup steps here.
+ */
+
+ // In our case, we're going to add a callback function that will get called
+ // whenever the AccountLogin event is raised (magic).
+ EventSink.AccountLogin += AccountLogin;
+ }
+
+ public static void Initialize()
+ {
+ /*
+ We don't actually use this here, but it's important to know about the Initialize function.
+ After the world loads, but before the game starts, this function is called once.
+ Do setup steps here.
+ */
+ }
+```
+
+#### Do Something
+```csharp
+ // This is the function we added to the AccountLogin event above
+ public static void AccountLogin(AccountLoginEventArgs e)
+ {
+ // check if the account exists, if not, return
+ if (Accounts.GetAccount(e.Username) is not Account account)
+ {
+ return;
+ }
+
+ // check if account is young, if not we don't care
+ if (!account.Young)
+ {
+ return;
+ }
+
+ // remove the young status from the account
+ account.RemoveYoungStatus(0);
+ }
+```
+
+### Load Your Assembly
+
+The file `Distribution/Data/assemblies.json` determines what files ModernUO will load as addons. Replace it with the following to include our file `AddonExample.dll`.
+
+```json
+[
+ "UOContent.dll",
+ "AddonExample.dll"
+]
+```
+
+### Run It? I think
+
+I haven't fixed my ModernUO to build on dotnet 9 yet.