diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8674770..7d8fbd5 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -40,3 +40,18 @@ jobs: # - name: Run tests # run: dotnet test --no-build --verbosity normal + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install documentation dependencies + run: python -m pip install --requirement requirements-docs.txt + + - name: Build documentation + run: python -m mkdocs build --strict diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c82de69..11347ca 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,7 @@ jobs: prev_version: ${{ steps.version.outputs.prev_version }} release_tag: ${{ steps.version.outputs.release_tag }} api_changed: ${{ steps.api_check.outputs.api_changed }} + docs_release: ${{ steps.version.outputs.docs_release }} steps: - uses: actions/checkout@v4 with: @@ -52,12 +53,21 @@ jobs: | tail -n 1) fi + docs_release=false + if [[ -z "$previous_tag" || "${RELEASE_TAG%.*}" != "${previous_tag%.*}" ]]; then + docs_release=true + fi + if ! git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then + docs_release=true + fi + echo "Release version: $RELEASE_TAG" echo "Previous release tag: ${previous_tag:-none}" { echo "release_tag=$RELEASE_TAG" echo "new_version=$RELEASE_TAG" echo "prev_version=$previous_tag" + echo "docs_release=$docs_release" } >> "$GITHUB_OUTPUT" - name: Setup NuGet source @@ -197,6 +207,38 @@ jobs: files: | StarMap-${{ needs.build.outputs.new_version }}.zip + publish-docs: + runs-on: ubuntu-latest + needs: [build, release-zip] + if: needs.build.outputs.docs_release == 'true' + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install documentation dependencies + run: python -m pip install --requirement requirements-docs.txt + + - name: Configure Git identity + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Publish versioned documentation + env: + RELEASE_VERSION: ${{ needs.build.outputs.new_version }} + run: | + docs_version="${RELEASE_VERSION%.*}" + mike deploy --push --update-aliases "$docs_version" latest + mike set-default --push latest + build-release-installer: runs-on: windows-latest needs: [build, release-zip] diff --git a/.gitignore b/.gitignore index d2223bd..07d8c1c 100644 --- a/.gitignore +++ b/.gitignore @@ -363,3 +363,4 @@ MigrationBackup/ FodyWeavers.xsd installer/dist +/site/ diff --git a/README.md b/README.md index 643fa5d..f1ae064 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,16 @@ A POC/Prototype arbitrary code modloader for Kitten Space Agency. Currently this loader can be ran with this functionality in the background, or as a dumb loader just loading mods. It makes use of Assembly Load Contexts to ensure mod dependencies are managed seperatly, reducing conflicts +## Documentation + +The [StarMap documentation](https://starmaploader.github.io/StarMap/) covers installation, usage, and mod development. Its Markdown source is in [`docs`](docs/). + ## Releases To publish a release, create and push a semantic-version tag such as `1.2.3` on the commit to release. The release workflow builds the release and installer from that commit; tags can point to commits on any branch. +The documentation site keeps a version for each major/minor release and updates the `latest` version alias. + ## Installation - Download and unzip release from [Releases](https://github.com/StarMapLoader/StarMap/releases/latest). diff --git a/StarMap.API/README.md b/StarMap.API/README.md index 0a90ade..c0f60d2 100644 --- a/StarMap.API/README.md +++ b/StarMap.API/README.md @@ -1,222 +1,9 @@ # StarMap API -This package provides the API for mods to interface with the [StarMap](https://github.com/StarMapLoader/StarMap) modloader. +`StarMap.API` is the public API package for mods loaded by StarMap. It targets .NET 10. -## How to create mods +For the current, versioned modding guide—including project setup, lifecycle callbacks, services, and dependencies—see the [StarMap documentation](https://starmaploader.github.io/StarMap/latest/modding/). -### General architecture +The package version is not guaranteed to match the StarMap application version. Use a `StarMap.API` version compatible with the loader you intend to support. -A StarMap mod is in essence an extension of KSA mods. -Every mod will at minimum contain a mod.toml (which is also the case for KSA mods) as well as an entry assembly. - -#### mod.toml - -While it is not stricly neccesary, it is advised to add StarMap info to the mod.toml, at its most basic, a mod.toml should like like this: - -```toml -name = "MyAmazingMod" - -[StarMap] -EntryAssembly = "MyAmazingMod" -``` - -The name will be the modid of your mod, this should be the same name as the folder in which this lives in the Content folder. -The "StarMap" section is optional, if it is provided, at mimimum, it should provide the name of the assembly which StarMap will initially load (without the dll extension). -If it is not provided, StarMap will search for a [modid].dll in the mod folder. - -#### Entry assembly - -The above mentioned entry assembly will be the first thing that StarMap will load. -Within this assembly, StarMap will search for a class that has the [StarMapMod](#starmapmod) attribute. It will use the first that it finds. -This class will be the core part of the mod, any entry from StarMap will be done through this class. -To add functionality to the mod, methods with attributes can be added, which attributes are available and what their functionality is, can be found in the [Attributes API reference](#attributes). -These methods will be called as instance methods on the mod class, throughout the runtime of KSA, StarMap will create once instance of this class and reuse it. -The implemenation should follow the signatures shown in the example code. - -### Dependencies - -In many cases, mods will have dependencies on other mods, or will want to overwrite functionality of other mods. -To achieve this, some extra configuration is required, this configuration is confined to the mod.toml within the StarMap section. - -#### Exported assemblies - -First of all, mods can configure what assemblies should be exposed to other mods, by default all assemblies that are provided with the mod can be accessed from other mods, this can be changed with the ExportedAssemblies. -In below example, only the StarMap.SimpleMod.Dependency(.dll) assembly will be accessable from other mods (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). - -```toml -name = "MyOtherAmazingMod" - -[StarMap] -EntryAssembly = "MyOtherAmazingMod" -ExportedAssemblies = [ - "MyDependency" -] -``` - -#### Mod dependency - -Then, mods can define what mods they want to depend on, they can do this by adding a new ModDependencies list entry in the mod.toml - -```toml -name = "MyAmazingMod" - -[StarMap] -EntryAssembly = "MyAmazingMod" - -[[StarMap.ModDependencies]] -ModId = "MyOtherAmazingMod" -Optional = false -ImportedAssemblies = [ - "MyDependency" -] -``` - -In above example, it is provided that StarMap.SimpleMod wants to depend on StarMap.SimpleMod2, this dependency is not optional and the mod wants to access the StarMap.SimpleMod.Dependency assembly. -Following fields can be used - -- The `ModId` should be the same as is provided as the name field in the mod.toml of the dependency mod. -- The `Optional` field (default false) defines if this dependency is optional, more info in the [loading strategy](#dependency-loading-strategy) -- The `ImportedAssemblies` field contains a list of assemblies that this mod intends to use from the dependency (more info in [imported and exported assemblies](#imported-and-exported-assemblies)). - -#### Imported and exported assemblies - -The goal of the imported and exported assembly fields is to compile a list of assemblies that will be provided to a mod from a dependency, below is the behaviour depending on the content of both fields: - -- If both fields are not filled in, the list will contain the entry assembly of the dependency. -- If only 1 of the lists is filled in, it will use this list to provide the assemblies. -- If both lists are defined, the intersection of the two will be used. - -## Mod loading strategy - -When StarMap is started, it will start with loading the manifest.toml (in the same way KSA does it, only sooner), it will then start loading mods from top to bottom. -It will first load the mod.toml, if the file does not exists, mod loading will not work, if there is no StarMap section, it will use a default configuration. -Using the config, if there are dependencies, it will first check if these dependencies are already loaded. If they are all loaded, mod loading continues, -otherwise, it stores what dependencies are still needed and continues to the next mod. -It will then search for the entry assembly, if it does not exists, loading will be stopped, otherwise, it will load the assembly. Then will search for a class with the StarMapMod attribute and create an instance. With the instance, it goes over the known attributes and stores a reference to the methods, allowing for quick quering, it stores the StarMapBeforeMain and StarMapImmediateLoad methods seperatly because they are mod specific. -Once the mod is fully loaded, it will call the StarMapBeforeMain method, if there is any. - -Now that the mod has been loaded, it checks the list of mods that are waiting for dependencies, and if there are any that are waiting for this mod. If so, it removes itself from the waiting dependencies and checks if the mod can now be loaded, if so, the mod is loaded and the StarMapBeforeMain of that mod is called. - -It does this for all the mods in the manifest. -Once it has tried loading all the mods, it gets the mods that are still waiting and checks them again. -If for a waiting mod, all its dependencies are optional, it will now load this mod. The implementation of the mod should ensure it can handle the optional dependency can be absent. -It keeps looping over the list of waiting mods until it has gone through the list once without being able to load a new mod, this indicates there are no more mods that can load with the provided mods, and gives up on loading these mods. - -Now StarMap will start KSA, which in turn will call StarMapImmediateLoad for each mod, if implemented. - -## Examples - -Some examples can be found in the [example mods repository](https://github.com/StarMapLoader/StarMap-ExampleMods) - -## API reference - -### Attributes - -#### StarMapMod - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Class - -Marks the main class for a StarMap mod. -Only attributes on methods within classes marked with this attribute will be considered. - -```csharp -[StarMapMod] -public class ModClass -``` - -#### StarMapBeforeMain - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Method - -Methods marked with this attribute will be called before KSA is started. - -```csharp -[StarMapBeforeMain] -public void ModMethod() -``` - -#### StarMapImmediateLoad - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Method - -Methods marked with this attribute will be called immediately when the mod is loaded by KSA. -It is called before the `KSA.Mod.PrepareSystems` method for each mod - -```csharp -[StarMapBeforeMain] -public void ModMethod(KSA.Mod mod) -``` - -#### StarMapAllModsLoaded - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Method - -Methods marked with this attribute will be called when all mods are loaded. -It is called after the `KSA.ModLibrary.LoadAll` method. - -```csharp -[StarMapAllModsLoaded] -public void ModMethod() -``` - -#### StarMapUnload - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Method - -Methods marked with this attribute will be called when KSA is unloaded - -```csharp -[StarMapUnload] -public void ModMethod() -``` - -#### StarMapBeforeGui - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Method - -Methods marked with this attribute will be called before KSA starts creating its ImGui interface. -It is called just before the `KSA.Program.OnDrawUi` method. - -```csharp -[StarMapBeforeGui] -public void ModMethod(double dt) -``` - -#### StarMapAfterGui - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Method - -Methods marked with this attribute will be called when KSA has finished creating its ImGui interface. -It is called just after the `KSA.Program.OnDrawUi` method. - -```csharp -[StarMapAfterGui] -public void ModMethod(double dt) -``` - -#### StarMapAfterOnFrame - -Namespace: `StarMap.API` -Assembly: `StarMap.API` -Target: Method - -Methods marked with this attribute will be called after `KSA.Program.OnFrame` is called. - -```csharp -[StarMapAfterOnFrame] -public void ModMethod(double currentPlayerTime, double dtPlayer); -``` +Additional examples are available in the [StarMap-ExampleMods repository](https://github.com/StarMapLoader/StarMap-ExampleMods). diff --git a/StarMap.Core/ModRepository/RuntimeMod.cs b/StarMap.Core/ModRepository/RuntimeMod.cs index d158bf7..360cfe8 100644 --- a/StarMap.Core/ModRepository/RuntimeMod.cs +++ b/StarMap.Core/ModRepository/RuntimeMod.cs @@ -68,6 +68,7 @@ public static bool TryCreateMod(ModEntry manifestEntry, AssemblyLoadContext core ModAssemblyLoadContext = modLoadContext, ModAssembly = modAssembly, Config = starMapConfig, + ExportedAssemblies = [.. starMapConfig.ExportedAssemblies], }; modLoadContext.RuntimeMod = runtimeMod; diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..12b160f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,14 @@ +# StarMap + +StarMap is a .NET mod loader for Kitten Space Agency (KSA). It loads mods listed by KSA and gives mod assemblies lifecycle hooks and services through the `StarMap.API` package. Each mod is loaded in its own .NET assembly load context to help isolate its dependencies. + +StarMap is an actively developed prototype. Check that a mod targets a compatible StarMap/API release and KSA version. + +## Get started + +- [Install StarMap](installation.md) +- [Use StarMap and install mods](using-starmap.md) +- [Create your first mod](modding/first-mod.md) +- [Browse the modding overview and API](modding/index.md) + +The version selector in the header lets you view documentation for a particular major/minor release. `latest` points to the most recently published documentation version. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..08e70d7 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,36 @@ +# Installation + +## Requirements + +- Windows x64 +- Kitten Space Agency installed +- The [.NET 10 Runtime for Windows x64](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) + +StarMap is framework-dependent, so the .NET runtime must be installed separately. + +## Install StarMap + +1. Download the latest ZIP or Windows installer from [StarMap Releases](https://github.com/StarMapLoader/StarMap/releases/latest). +2. For the ZIP, extract it to a folder and run `StarMap.exe` from that folder. For the installer, follow its prompts and select the KSA folder containing `KSA.dll` when asked. +3. If you installed from the ZIP, the first launch creates `StarMapConfig.json` and exits. Open that file, set `GameLocation` as described below, and save it. The installer normally creates this file for you. +4. Run `StarMap.exe` again. With a valid configuration, it starts KSA through StarMap. + +Keep `StarMapConfig.json` beside `StarMap.exe` and start StarMap from its installation folder. + +## Configure the KSA path + +`GameLocation` may be either the KSA installation folder or the full path to `KSA.dll`. If you provide a folder, StarMap looks for `KSA.dll` inside it. + +```json +{ + "GameLocation": "C:\\Games\\Kitten Space Agency", + "RepositoryLocation": "", + "GameArguments": [] +} +``` + +- `GameLocation` is required. +- `RepositoryLocation` is reserved and currently unused; leave it empty. +- `GameArguments` is optional and passes additional command-line arguments to KSA. + +If StarMap reports that it cannot find `KSA.dll`, check the spelling and confirm that `GameLocation` points to the KSA folder or the DLL itself. diff --git a/docs/maintainers.md b/docs/maintainers.md new file mode 100644 index 0000000..efb94b7 --- /dev/null +++ b/docs/maintainers.md @@ -0,0 +1,36 @@ +# Documentation maintenance + +## Work on the docs locally + +Install the Python documentation dependencies and serve the site: + +```powershell +python -m pip install --requirement requirements-docs.txt +python -m mkdocs serve +``` + +Before opening a pull request, run the same strict build used by CI: + +```powershell +python -m mkdocs build --strict +``` + +Keep documentation changes in the same pull request as the feature or behavior change they describe. The PR workflow builds the documentation to catch configuration errors and warnings. + +## Versioned publishing + +The release workflow runs from semantic-version tags in `X.Y.Z` format. It publishes a documentation snapshot when the major/minor portion changes from the previous reachable release tag. Mike names the snapshot `X.Y`, preserves older snapshots, updates the `latest` alias, and makes that alias the site's default. Patch releases do not create additional snapshots. If the `gh-pages` branch does not exist yet, the first tagged release bootstraps the documentation site. + +The workflow publishes documentation after the release ZIP has been created. It pushes generated site files to the `gh-pages` branch; the source Markdown remains in `docs/`. + +After the first deployment, configure GitHub Pages once in the repository settings: + +1. Open **Settings → Pages**. +2. Under **Build and deployment**, select **Deploy from a branch**. +3. Select the `gh-pages` branch and the `/(root)` folder, then save. + +The site URL configured in `mkdocs.yml` is `https://starmaploader.github.io/StarMap/`. + +## Theme maintenance + +The current Material for MkDocs release announces end-of-life on May 5, 2027. Mike provides the versioning workflow separately from the theme, so review theme support before that date. The [Zensical MkDocs compatibility guide](https://zensical.org/docs/compatibility/mkdocs/migration/) describes the successor project's migration path. diff --git a/docs/modding/constructor-injection.md b/docs/modding/constructor-injection.md new file mode 100644 index 0000000..a4876f0 --- /dev/null +++ b/docs/modding/constructor-injection.md @@ -0,0 +1,36 @@ +# Constructor injection + +StarMap creates one instance of each mod's `[StarMapMod]` entry class with `ActivatorUtilities`. Before creating the instance, it creates a dependency-injection scope for that mod and sets the mod context used by the services. + +The services currently registered in that scope are: + +- `IModMenuService` for adding items to KSA's **Mods** menu +- `IMediationService` for publishing and subscribing to typed events +- `IPatchService` for registering method patches + +## Request services + +Declare the services a mod needs as parameters of the entry class's public constructor: + +```csharp +using StarMap.API; +using StarMap.API.Services; + +[StarMapMod] +public sealed class ExampleMod +{ + private readonly IModMenuService _menu; + + public ExampleMod(IModMenuService menu) + { + _menu = menu; + } +} +``` + +Use one public constructor, and ensure its parameters can be resolved from the services StarMap registers. There is no public API for a mod to register its own services. + +Constructor injection applies only to the `[StarMapMod]` entry class. Lifecycle callbacks are called with their documented fixed signatures; StarMap does not inject their parameters. See [lifecycle callbacks](lifecycle.md). + +Each mod has its own dependency-injection scope. Keep injected services on the entry class instance, and dispose handles returned by services, such as menu items, event publishers, and event subscriptions, in `[StarMapUnload]`. + diff --git a/docs/modding/dependencies.md b/docs/modding/dependencies.md new file mode 100644 index 0000000..cc8c03a --- /dev/null +++ b/docs/modding/dependencies.md @@ -0,0 +1,47 @@ +# Mod dependencies + +Declare StarMap settings and dependencies in the mod's `mod.toml`. Dependency IDs are KSA mod IDs: they must match the `name` in the dependency's `mod.toml` and its ID in KSA's manifest. + +```toml +name = "ConsumerMod" + +[StarMap] +EntryAssembly = "ConsumerMod" +ExportedAssemblies = [ + "ConsumerMod.Contracts" +] + +[[StarMap.ModDependencies]] +ModId = "ProviderMod" +Optional = false +ImportedAssemblies = [ + "ProviderMod.Contracts" +] +``` + +- `EntryAssembly` names the entry DLL without the `.dll` extension. If omitted with the `[StarMap]` section, it defaults to the mod ID. +- `ExportedAssemblies` lists assemblies this mod makes available to dependents. +- `ModDependencies` may contain multiple `[[StarMap.ModDependencies]]` tables. +- `ModId` identifies the dependency. +- `Optional` defaults to `false`. A required dependency must be present for the dependent mod to initialize. An optional dependency may be absent, so the mod must tolerate its API not being available. +- `ImportedAssemblies` lists assembly names the dependent mod needs from that dependency. + +Use assembly names without the `.dll` suffix. The assemblies must be present in the corresponding mod's output folder. + +## Which assemblies are shared? + +For each dependency, StarMap chooses usable assemblies as follows: + +- If neither `ImportedAssemblies` nor `ExportedAssemblies` is set, only the dependency's entry assembly is shared. +- If only one list is set, that list is used. +- If both lists are set, only their intersection is shared. + +This lets a provider expose a small contracts assembly instead of sharing all of its implementation. Declare the provider as a dependency before referencing its types, and mark it non-optional if the consumer requires those types to load. + +StarMap loads each mod in a separate assembly load context. Declare inter-mod references explicitly; do not rely on an undeclared mod's assembly being found. + +## Sharing event types between mods + +When using [`IMediationService`](event-mediation.md), the event type must be defined in an assembly loaded by the publishing mod and shared with each subscribing mod. Add that assembly to the provider's `ExportedAssemblies` and to the consumer's `ImportedAssemblies`; make the provider a required dependency if the consumer subscribes during startup. + +The consumer also needs a compile-time reference to the provider's event contract assembly. See [typed event mediation](event-mediation.md) for publisher and subscriber examples. diff --git a/docs/modding/event-mediation.md b/docs/modding/event-mediation.md new file mode 100644 index 0000000..192a623 --- /dev/null +++ b/docs/modding/event-mediation.md @@ -0,0 +1,100 @@ +# Typed event mediation + +`IMediationService` lets mods publish and subscribe to typed events. Event types implement `IStarMapEvent`. Define an event type in an assembly loaded by the publishing mod: + +```csharp +using StarMap.API.Mediation; + +public sealed record SelectionChanged(string Selection) : IStarMapEvent; +``` + +## Publishing an event + +The mod that owns the event type registers its publisher once, then publishes messages: + +```csharp +using StarMap.API; +using StarMap.API.Mediation; +using StarMap.API.Services; + +[StarMapMod] +public sealed class SelectionPublisher +{ + private readonly IMediationService _events; + private IEventPublisher? _publisher; + + public SelectionPublisher(IMediationService events) + { + _events = events; + } + + [StarMapBeforeMain] + public void Initialize() + { + _publisher = _events.RegisterPublisher(); + } + + public void ChangeSelection(string selection) + { + _publisher?.Publish(new SelectionChanged(selection)); + } + + [StarMapUnload] + public void Unload() + { + _publisher?.Dispose(); + } +} +``` + +Only the mod that owns the event type's assembly can register its publisher, and only one publisher can be registered for each event type. Attempting to register another publisher throws. + +## Subscribing to an event + +A dependent mod can subscribe after the publisher has registered: + +```csharp +using System; +using StarMap.API; +using StarMap.API.Mediation; +using StarMap.API.Services; + +[StarMapMod] +public sealed class SelectionSubscriber +{ + private readonly IMediationService _events; + private ISubscription? _subscription; + + public SelectionSubscriber(IMediationService events) + { + _events = events; + } + + [StarMapBeforeMain] + public void Initialize() + { + _subscription = _events.Subscribe(OnSelectionChanged); + } + + private void OnSelectionChanged(SelectionChanged message) + { + Console.WriteLine($"Selection changed to {message.Selection}"); + } + + [StarMapUnload] + public void Unload() + { + _subscription?.Dispose(); + } +} +``` + +`Subscribe()` throws if a publisher for `T` has not been registered yet. Dispose subscriptions and the publisher when their respective mods unload. + +## Synchronous and parallel publishing + +`Publish` invokes subscribers synchronously. `PublishParallel` runs each handler concurrently and returns a `Task` that completes when the handlers finish. Handlers used with `PublishParallel` must be thread-safe and should not access game state unless that access is safe from a worker thread. + +## Sharing event types between mods + +The event type's assembly must be shared with each subscribing mod so both mods use the same event type. Add it to the provider's `ExportedAssemblies` and the consumer's `ImportedAssemblies`. The consumer must also declare the provider as a required dependency if it subscribes during startup. See [mod dependencies](dependencies.md#sharing-event-types-between-mods) for the configuration example. diff --git a/docs/modding/first-mod.md b/docs/modding/first-mod.md new file mode 100644 index 0000000..99f6ad5 --- /dev/null +++ b/docs/modding/first-mod.md @@ -0,0 +1,71 @@ +# Create your first mod + +This walkthrough builds a small mod that logs a message before KSA starts. It uses a lifecycle callback and does not require constructor injection. + +## Create a project + +Install the .NET 10 SDK, then create a class library and add the StarMap API package: + +```powershell +dotnet new classlib --framework net10.0 --name HelloStarMap +cd HelloStarMap +dotnet add package StarMap.API +``` + +Use the StarMap.API package version compatible with the loader you are targeting. It may have a different version number from the StarMap application. Add `x64` to the project's main ``: + +```xml + + net10.0 + x64 + enable + enable + +``` + +The project name produces `HelloStarMap.dll` by default, which will match the entry assembly name below. + +## Add the entry class + +Replace the generated class with: + +```csharp +using StarMap.API; + +[StarMapMod] +public sealed class HelloStarMap +{ + [StarMapBeforeMain] + public void Initialize() + { + Console.WriteLine("Hello from my mod."); + } +} +``` + +This entry class uses a parameterless constructor. For constructor injection and the services registered by StarMap, see the [constructor-injection guide](constructor-injection.md). + +## Add `mod.toml` + +Create `mod.toml` in the project root: + +```toml +name = "HelloStarMap" + +[StarMap] +EntryAssembly = "HelloStarMap" +``` + +`name` is the KSA mod ID. Keep it the same as the mod folder and KSA manifest ID. `EntryAssembly` is the assembly name without `.dll`. + +## Build and install + +Build the project: + +```powershell +dotnet build -c Release +``` + +Copy `mod.toml`, the built `HelloStarMap.dll`, its `.deps.json`, and any private dependencies from `bin\Release\net10.0\` into a folder named `HelloStarMap` in either [supported mod location](../using-starmap.md#install-and-enable-a-mod). Keep `mod.toml` beside the DLL. Enable the matching mod ID in KSA's manifest, then start the game with `StarMap.exe`. + +When loaded, the mod writes its message to the StarMap console before KSA starts. diff --git a/docs/modding/index.md b/docs/modding/index.md new file mode 100644 index 0000000..a9c890a --- /dev/null +++ b/docs/modding/index.md @@ -0,0 +1,30 @@ +# Modding overview + +A StarMap mod extends the KSA mod format. A basic mod consists of: + +1. A KSA manifest entry that enables the mod. +2. A mod folder named for the mod ID, containing `mod.toml`. +3. A .NET 10 class-library assembly named by `EntryAssembly`. +4. A class marked with `[StarMapMod]`. + +```text +HelloStarMap/ +|-- mod.toml +|-- HelloStarMap.dll +|-- HelloStarMap.deps.json +`-- any private dependency assemblies +``` + +Start with the [first-mod walkthrough](first-mod.md). Then read about [lifecycle callbacks](lifecycle.md), [services and mechanics](services.md), and [mod dependencies](dependencies.md). The service overview links to separate guides for [constructor injection](constructor-injection.md), [menu items](mod-menu.md), [event mediation](event-mediation.md), and [method patching](patching.md). + +## Entry assembly and mod ID + +The `name` in `mod.toml` is the KSA mod ID. It must match the mod's folder name and its ID in KSA's manifest. `EntryAssembly` is the assembly name without the `.dll` suffix. If the `[StarMap]` section is omitted, StarMap defaults the entry assembly name to the mod ID; explicitly setting it is recommended. + +StarMap creates one instance of the marked entry class for the mod. Keep one `[StarMapMod]` class in the entry assembly so there is no ambiguity about which class StarMap selects. + +## Build target + +StarMap and `StarMap.API` target .NET 10. Build mods as .NET 10 class libraries for x64, and use the `StarMap.API` NuGet package compatible with the loader version you intend to support. + +For additional working examples, see the [StarMap-ExampleMods repository](https://github.com/StarMapLoader/StarMap-ExampleMods). The versioned API documentation here describes the attribute-based entry points used by current StarMap releases. diff --git a/docs/modding/lifecycle.md b/docs/modding/lifecycle.md new file mode 100644 index 0000000..0dfe2af --- /dev/null +++ b/docs/modding/lifecycle.md @@ -0,0 +1,21 @@ +# Lifecycle callbacks and services + +## Entry point + +Mark the entry class with `[StarMapMod]`. StarMap creates one instance for the mod and scans its methods for lifecycle attributes. Use public instance methods with the signatures shown below. + +| Attribute | Required method signature | Called | +| --- | --- | --- | +| `[StarMapBeforeMain]` | `void Method()` | After the mod is initialized and before KSA starts. | +| `[StarMapImmediateLoad]` | `void Method(KSA.Mod definingMod)` | Immediately before KSA prepares that mod's systems. | +| `[StarMapAllModsLoaded]` | `void Method()` | After KSA's `ModLibrary.LoadAll` completes. | +| `[StarMapUnload]` | `void Method()` | When KSA unloads. Dispose mod-owned subscriptions and menu items here. | +| `[StarMapBeforeGui]` | `void Method(double dt)` | Before KSA's GUI frame callback. | +| `[StarMapAfterGui]` | `void Method(double dt)` | After KSA's GUI drawing pass. | +| `[StarMapAfterOnFrame]` | `void Method(double currentPlayerTime, double dtPlayer)` | After KSA's `Program.OnFrame`. | + +The callback methods return `void`. Use the exact parameter types and counts; methods with a different signature are ignored. + +## Constructor-injected services + +StarMap provides `IModMenuService`, `IMediationService`, and `IPatchService` through the entry class constructor. See [constructor injection](constructor-injection.md) for the injection mechanism and the individual pages for [menu items](mod-menu.md), [event mediation](event-mediation.md), and [method patching](patching.md) for their usage. diff --git a/docs/modding/mod-menu.md b/docs/modding/mod-menu.md new file mode 100644 index 0000000..37e718d --- /dev/null +++ b/docs/modding/mod-menu.md @@ -0,0 +1,39 @@ +# In-game menu items + +`IModMenuService.AddModMenuItem(string label, Action callback)` adds an item to KSA's **Mods** menu. The label must be unique across mods; registering a duplicate label throws. The returned `IDisposable` removes the item. + +Inject `IModMenuService` into the `[StarMapMod]` entry class, register the item during initialization, and dispose its handle when the mod unloads: + +```csharp +using System; +using StarMap.API; +using StarMap.API.Services; + +[StarMapMod] +public sealed class ExampleMod +{ + private readonly IModMenuService _menu; + private IDisposable? _menuItem; + + public ExampleMod(IModMenuService menu) + { + _menu = menu; + } + + [StarMapBeforeMain] + public void Initialize() + { + _menuItem = _menu.AddModMenuItem( + "Example action", + () => Console.WriteLine("Selected from the Mods menu.")); + } + + [StarMapUnload] + public void Unload() + { + _menuItem?.Dispose(); + } +} +``` + +The callback runs when the player selects the item. For the constructor-injection mechanism, see [constructor injection](constructor-injection.md). diff --git a/docs/modding/patching.md b/docs/modding/patching.md new file mode 100644 index 0000000..24c53ed --- /dev/null +++ b/docs/modding/patching.md @@ -0,0 +1,71 @@ +# Method patching + +`IPatchService` registers prefix or postfix callbacks against a `MethodBase`. It supports static and instance methods, with or without return values. Callbacks receive the target method's arguments; instance callbacks also receive the target object, and callbacks for methods with return values can read or change the result by reference. + +## Choose a callback type + +| Target method | Registration method | Callback parameters | +| --- | --- | --- | +| Static, returns `void` | `RegisterStaticVoidPatch` | `object[] args` | +| Static, returns a value | `RegisterStaticPatch` | `object[] args`, `ref TResult result` | +| Instance, returns `void` | `RegisterInstanceVoidPatch` | `object[] args`, `TInstance instance` | +| Instance, returns a value | `RegisterInstancePatch` | `object[] args`, `TInstance instance`, `ref TResult result` | + +`TInstance` is the target method's instance type. `TResult` must match the target method's return type. The argument array contains the original method arguments in declaration order. + +## Prefix example + +A prefix runs before the target method. Return `true` to continue to the next prefix and then the original method. Returning `false` stops later prefixes and skips the original method. + +```csharp +using System; +using System.Reflection; +using StarMap.API; +using StarMap.API.Services; + +public static class ExampleTarget +{ + public static void Run(int value) + { + Console.WriteLine($"Running with {value}"); + } +} + +[StarMapMod] +public sealed class ExampleMod +{ + private readonly IPatchService _patches; + + public ExampleMod(IPatchService patches) + { + _patches = patches; + } + + [StarMapBeforeMain] + public void Initialize() + { + MethodInfo target = typeof(ExampleTarget).GetMethod( + nameof(ExampleTarget.Run), + BindingFlags.Public | BindingFlags.Static) + ?? throw new MissingMethodException( + typeof(ExampleTarget).FullName, + nameof(ExampleTarget.Run)); + + _patches.RegisterStaticVoidPatch( + IPatchService.PatchType.Prefix, + target, + args => + { + var value = (int)args[0]; + Console.WriteLine($"Before Run({value})"); + return value >= 0; + }); + } +} +``` + +## Postfixes and return values + +A postfix runs after the target method. Returning `false` from a postfix stops later postfix callbacks; the original method has already run. For a method that returns a value, use a result callback and modify `result` by reference when needed. Its generic `TResult` must exactly match the target method's return type. + +When selecting a target with reflection, choose the intended overload explicitly. Prefer lifecycle callbacks where they meet the need; game-internal patch targets can vary across KSA versions. diff --git a/docs/modding/services.md b/docs/modding/services.md new file mode 100644 index 0000000..9098f06 --- /dev/null +++ b/docs/modding/services.md @@ -0,0 +1,10 @@ +# Services and mechanics + +StarMap provides three services to mod entry classes through constructor injection. Each has its own guide: + +- [Constructor injection](constructor-injection.md) +- [In-game menu items](mod-menu.md) with `IModMenuService` +- [Typed event mediation](event-mediation.md) with `IMediationService` +- [Method patching](patching.md) with `IPatchService` + +For entry-point method signatures, see [lifecycle callbacks](lifecycle.md). For declaring mod dependencies and sharing assemblies, see [mod dependencies](dependencies.md). diff --git a/docs/using-starmap.md b/docs/using-starmap.md new file mode 100644 index 0000000..334dc3b --- /dev/null +++ b/docs/using-starmap.md @@ -0,0 +1,32 @@ +# Using StarMap + +Start the game by running `StarMap.exe`, not `KSA.exe`. StarMap reads `StarMapConfig.json`, prepares enabled mods from KSA's manifest, then starts the game. + +## Install and enable a mod + +Mods are ordinary KSA mods with a `mod.toml` and a StarMap entry assembly. Place each mod in one of these locations: + +- `%USERPROFILE%\Documents\My Games\Kitten Space Agency\mods\\` — recommended; separate from the game installation. +- `\Content\\` + +The folder name and KSA manifest mod ID must match. StarMap discovers mods through KSA's manifest; placing a folder by itself does not add or enable the mod. Enable the mod using KSA's normal manifest/mod-management process. StarMap follows the manifest's mod order and resolves declared dependencies while loading. + +See [Create your first mod](modding/first-mod.md) for the required files and [Mod dependencies](modding/dependencies.md) for dependency configuration. + +## Custom KSA documents path + +By default, KSA uses its usual documents location for local mods. StarMap also supports an instance path override if you keep that data elsewhere: + +- Pass `-InstancePath ` as a game argument, or +- Set the `STARMAP_INSTANCE_PATH` environment variable. + +For example, the `GameArguments` setting can include `["-InstancePath", "D:\\KSAProfiles\\Test"]`. + +## Troubleshooting + +- **StarMap exits after the first launch:** edit the generated `StarMapConfig.json` and set `GameLocation`. +- **KSA does not start:** confirm the .NET 10 x64 runtime is installed and `GameLocation` resolves to `KSA.dll`. +- **A mod is not loaded:** confirm it is enabled in KSA's manifest, the manifest ID matches the mod folder, and the folder contains `mod.toml` plus the configured entry DLL. Check the StarMap console output for load or dependency errors. +- **A required dependency is missing:** install and enable that mod, then check its ID and any imported assembly names in the dependent mod's `mod.toml`. + +StarMap currently loads local mods; it does not download mods from a remote catalog. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..8c50898 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,46 @@ +site_name: StarMap +site_description: Installation, usage, and mod development documentation for StarMap. +site_url: https://starmaploader.github.io/StarMap/ +repo_url: https://github.com/StarMapLoader/StarMap +repo_name: StarMapLoader/StarMap +edit_uri: edit/main/docs/ + +theme: + name: material + features: + - navigation.sections + - content.code.copy + +plugins: + - search + - mike: + canonical_version: latest + version_selector: true + +markdown_extensions: + - admonition + - attr_list + - tables + - pymdownx.highlight + - pymdownx.superfences + +extra: + version: + provider: mike + +nav: + - Home: index.md + - Installation: installation.md + - Using StarMap: using-starmap.md + - Modding: + - Overview: modding/index.md + - First mod: modding/first-mod.md + - Lifecycle callbacks: modding/lifecycle.md + - Services and mechanics: + - Overview: modding/services.md + - Constructor injection: modding/constructor-injection.md + - In-game menu items: modding/mod-menu.md + - Typed event mediation: modding/event-mediation.md + - Method patching: modding/patching.md + - Dependencies: modding/dependencies.md + - Maintainers: maintainers.md diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..f40a85f --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,2 @@ +mkdocs-material>=9.5,<10 +mike>=2.1,<3