Skip to content

[API/Architecture] Modular compatibility SDK and conditional built-in modules #32

Description

@swear01

Goal

Turn Auto Storage's cross-mod support into a documented, versioned addon SDK and a set of conditionally loaded compatibility modules, so bundled integrations and third-party addon mods use the same bounded server-authoritative contracts.

The player still installs one Auto Storage jar. Bundled compatibility code is present in that jar but its classes are not loaded unless every required target mod is present. External authors ship an ordinary NeoForge addon mod and register through the public API; Auto Storage does not scan arbitrary jars or infer unsafe recipes.

Why this is needed

The current foundation is already strong:

  • custom registries exist for machine descriptors, exact recipe families, resource kinds, block strategies, and container strategies;
  • typed-resource transactions already support items, fluids, power, chemicals, and addon-defined resources;
  • deterministic recipe plans already model consumed inputs, catalysts, tools, remainders, multiple outputs, station work, and rollback.

The remaining problem is organization and developer ergonomics:

  • bundled modules are centrally enumerated in OptionalModRecipeCompatibility;
  • capability, block, container, recipe, variant, and recipe-reload hooks are split across several one-off dispatchers;
  • the main source set compiles against every supported optional mod;
  • TransformProviderApi is a global mutable list and machine-variant contribution is package-private;
  • the API is mixed with implementation classes and the existing API compile fixture can see the full main output;
  • adding one integration requires editing core dispatch code instead of adding one isolated module.

NeoForge explicitly recommends custom registries as the extension boundary for addon mods. AE2 publishes a distinct API surface and uses explicit loading-phase registration; RS2 separates API modules and exposes focused registries through one API entrypoint. Auto Storage should follow those patterns without copying their code or weakening its fail-closed crafting rules.

Architectural decisions

1. Keep granular registries; add one easy registration facade

Introduce a public com.swear.autostorage.api surface and one convenience entrypoint, provisionally:

AutoStorageAddon.register(MOD_ID, modBus, addon -> addon
        .machineDescriptors(MACHINES)
        .recipeFamilies(RECIPES)
        .resourceKinds(RESOURCE_KINDS)
        .containerStrategies(CONTAINERS)
        .blockStrategies(BLOCKS)
        .transformProviders(TRANSFORMS)
        .machineVariantContributors(VARIANTS));

The exact builder names may change during RED-first API tests, but the contract is fixed:

  • one call wires all addon-owned DeferredRegisters to the addon's mod bus;
  • stable IDs stay owned by the addon namespace;
  • the facade delegates to focused registries rather than becoming a mutable god object;
  • duplicate IDs, invalid timing, registry overflow, and incomplete contracts fail explicitly;
  • no API grants direct access to the Core map, player mutation, client authority, or transaction internals.

Existing low-level registry APIs remain available for advanced addons.

2. Make every extension point registry/lifecycle owned

  • Move Transform providers from the global mutable list to an ordered auto_storage:transform_provider registry.
  • Expose machine-variant contribution through a public ordered registry/API instead of package-private MachineVariantContributors.
  • Keep resource kinds, container strategies, block strategies, descriptors, and recipe families in their existing custom registries.
  • Add only bounded lifecycle hooks that are actually required, such as capability registration and server recipe-reload data refresh. Do not expose arbitrary tick/world callbacks.
  • Registration freezes before gameplay; runtime hot registration is not supported.

3. Compile-isolate bundled compatibility modules

Create an api source set plus one generated/configured source set per bundled integration. Each compatibility source set compiles against:

  • Auto Storage API/core outputs;
  • only its target mod's representative compile dependency;
  • no other optional integration.

The main source set must compile without Mekanism, Botania, Create, or any other optional mod API on its classpath. The final player artifact still merges all bundled module outputs into one Auto Storage jar.

Suggested layout:

src/api/java/com/swear/autostorage/api/...
src/main/java/com/swear/autostorage/...
src/compat/mekanism/java/...
src/compat/botania/java/...
src/compat/create/java/...
...

Gradle should generate the repetitive source-set/dependency wiring from one declarative module table rather than adding another hand-written block for every integration.

4. Use metadata-gated module discovery

Each bundled module owns metadata such as:

{
  "schema": 1,
  "id": "auto_storage:mekanism",
  "entrypoint": "com.swear.autostorage.compat.mekanism.MekanismCompatModule",
  "requires": ["mekanism"],
  "side": "both"
}

The build validates and aggregates those descriptors into a deterministic index. At runtime Auto Storage:

  1. reads metadata as strings;
  2. checks all required mod IDs through ModList;
  3. loads and instantiates the typed AutoStorageCompatModule entrypoint only when requirements are satisfied;
  4. reports the module ID and target mod ID on any linkage/registration failure;
  5. never catches an incompatibility and continues with partial support.

Do not use an annotation or ServiceLoader design that must classload the provider before checking the target mods. External addon jars do not need this bundled-module index: their own NeoForge mod entrypoint calls the public registration facade and declares Auto Storage/target dependencies in their own metadata.

5. Dogfood the public API

Migrate every current bundled integration to the same public contracts available to third parties:

  • Iron Furnaces
  • Farmer's Delight
  • Mekanism
  • Botania
  • Modern Industrialization
  • Ars Nouveau
  • EvilCraft
  • Powah
  • Industrial Foregoing
  • Create
  • Extended Crafting

PneumaticCraft remains an explicit present-mod fail-closed audit module until a safe supported family exists.

No bundled module may call a privileged private registration path. If a current integration cannot be expressed, first add the smallest reusable public contract with an external-addon regression test, then migrate the integration.

6. Publish a usable SDK

  • Produce a versioned auto_storage-<version>-api.jar plus sources/Javadocs while keeping the normal runtime mod jar.
  • Document a no-copy Gradle dependency using the public distribution/Maven path; compiling an addon must not require copying jars into libs/.
  • Add a minimal example addon that registers one station, one deterministic recipe family, one custom resource kind, one block/container strategy, one Transform provider, and one variant contribution.
  • Add an Addon Developer section to the GitHub Wiki and keep docs/ as the authoritative source.
  • Define API stability/versioning policy for alpha releases and clear failure behavior when a target mod changes incompatibly. Representative CI artifacts remain evidence only, never player-facing exact version pins.

Implementation phases

Phase A — API boundary and RED tests

  • Add an API-only compile fixture whose classpath cannot see implementation packages.
  • Write failing tests for one-call registration, stable IDs, duplicate rejection, freeze timing, dedicated-server safety, and forbidden implementation imports.
  • Move or wrap public contracts under com.swear.autostorage.api without exposing client-only classes.

Phase B — module loader and build isolation

  • Add validated per-module metadata and deterministic aggregation.
  • Add typed module entrypoint/context.
  • Move optional target dependencies off main and into isolated compatibility source sets.
  • Add bytecode/static guards proving common/API outputs do not reference third-party packages.

Phase C — complete extension surfaces

  • Registry-back Transform providers and machine-variant contributors.
  • Unify capability, block, container, recipe, and bounded recipe-reload registration under the module context.
  • Preserve server-owned simulate-then-commit behavior and exact sync/persistence IDs.

Phase D — migrate bundled integrations

  • Move modules one at a time, preserving existing fixture behavior and IDs.
  • Delete the central per-mod reflection switch and one-off optional dispatchers only after the final module passes.
  • Keep one-player-jar packaging and absent-mod startup behavior unchanged.

Phase E — SDK, documentation, and release gates

  • Publish API/sources/Javadocs artifacts.
  • Add the external example addon and developer guide.
  • Add API compatibility checks to CI.
  • Update release notes and Wiki when the API level changes.

Acceptance criteria

  • A different-package external fixture compiles using only the API artifact, not implementation output.
  • The example addon registers a custom resource, block/container transfer, station, recipe family, Transform provider, and existing-station variant through one facade.
  • The main/common/API bytecode has zero references to optional-mod packages or client-only Minecraft classes.
  • With target mods absent, no bundled compatibility entrypoint is classloaded and the dedicated GameTest server passes.
  • With each target mod present, exactly its module loads once and all current real behavior assertions pass.
  • A missing entrypoint, duplicate module ID, duplicate registry ID, malformed metadata, or binary-incompatible loaded target fails startup with the module/target identity and original cause.
  • Transform providers and machine-variant contributors are deterministic registry-owned entries, not global mutable/private side channels.
  • All current compatibility IDs, recipes, stations, typed resources, rates, remainders, catalysts, and rollback behavior remain unchanged after migration.
  • The all-mod compatibility matrix catches cross-module registration/classpath conflicts.
  • SelfTest, base/addon/optional GameTests, Python tests, build, API compile, and datagen drift stay green.
  • docs/ and the GitHub Wiki contain a complete copy-pasteable addon guide and API/versioning policy.
  • Release artifacts include runtime jar, API jar, API sources, and Javadocs with documented Gradle coordinates.

Non-goals

  • No generic Recipe#getIngredients()/reflection/EMI inference fallback.
  • No automatic support for arbitrary machines or recipes merely because a mod is installed.
  • No external-machine send-and-wait, asynchronous world processing, chance-output guessing, or arbitrary mutation callback.
  • No runtime script plugins or hot module registration after registries freeze.
  • No automatic mod download/installation.
  • No exposure of raw Core storage or client-owned storage state.
  • No optional-mod multi-version CI matrix and no player-facing exact target-mod version pins; other versions are handled through user reports and clear fail-closed errors.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions