Search is available after the production docs build.

Browse Docs
DocsDevelopersService Contracts

Developers

Service Contracts

Design reusable contracts that separate ECHO behavior from runtime-specific implementation details.

Purpose

Service contracts are the boundary between ECHO behavior and runtime-specific implementation. They let modules talk to shared systems without importing another module's internals or assuming one runtime lane.

The public SDK path uses Java contracts, descriptor metadata, and optional service lookup. First-party modules may contain com.knoxhack.* implementation classes, but public addon samples should stay on dev.echo.api.*, dev.echo.nativeplatform.contracts.*, and JDK types unless a guide says otherwise.

When To Create A Contract

Create a contract when more than one module needs the same behavior or when a player-facing surface needs data from another system.

Good candidates include:

  • Mission state and objective progress.
  • Index entries and unlock state.
  • Lens scan results.
  • HoloMap markers and overlays.
  • Persistent player, world, or team data.
  • Networked actions.
  • Package metadata used by PackOS or launcher tooling.

Avoid creating a contract only to rename a private method. A contract should describe platform behavior, not implementation trivia.

Contract IDs

Use stable IDs that include the owning module namespace and the contract domain:

  • echonetcore:network/packet_service
  • hello_content_addon:registry_service
  • echoterminal:terminal/surface
  • echolens:lens/scanners

Keep IDs lowercase and version behavior through docs, metadata, and migration notes instead of changing IDs casually.

Contract Metadata

Declare service boundaries in META-INF/echo.mod.json:

{
  "schema": "echo.mod.v1",
  "id": "echomissioncore",
  "requires": ["echoadaptercore", "echocore", "echonetcore"],
  "optional": ["echodatacore", "echoindex", "echoterminal", "echotutorialcore"],
  "provides": ["missions.objectives", "missions.routes"],
  "consumes": ["echo.core", "echo.net"],
  "permissions": ["missions.objectives", "missions.routes"],
  "apiStability": "beta"
}

This metadata is what reviewers, diagnostics, website pages, and release tooling can inspect without scanning every Java file.

Java Contract Shape

Define a narrow Java interface for behavior other modules need:

package dev.echo.example.contract;

import java.util.Optional;

public interface RelicScanService {
    boolean supports(String targetId);

    Optional<RelicScanResult> scan(String playerId, String targetId);
}

Use records for stable payloads:

package dev.echo.example.contract;

public record RelicScanResult(
        String title,
        String summary,
        String dangerLevel,
        String relatedIndexEntry
) {}

Keep runtime objects out of the contract whenever possible. Pass stable IDs, resource keys, or adapter references instead of loader-specific objects.

Service Registration

Register services through the runtime during addon initialization:

import dev.echo.nativeplatform.contracts.EchoNativeAddon;
import dev.echo.nativeplatform.contracts.EchoNativeAddonRuntime;

public final class RelicTechAddon implements EchoNativeAddon {
    @Override
    public void onInitialize(EchoNativeAddonRuntime runtime) {
        runtime.registerService(
                "echorelictech:relic_scan_service",
                new RelicScanServiceImpl()
        );
    }
}

For content-style SDK addons, use EchoRegistryContext to register blocks, items, recipes, and registry-backed descriptors. For service-style native addons, use EchoNativeAddonRuntime.registerService.

Optional Integration

Never hard-reference optional addons. Use optional service lookup or no-op fallback behavior:

Optional<IndexService> index = EchoOptionalServices.index();
index.ifPresent(service -> service.registerProvider(myDocsProvider));

The module descriptor should also list the optional module:

{
  "optional": ["echoindex"],
  "consumes": ["index.recipes"]
}

If a dependency is truly required, put it in requires and fail validation when it is missing.

Adapter Boundary

If a contract needs Minecraft, NeoForge, Native, or Standalone runtime data, isolate the translation close to AdapterCore.

Runtime references should stay narrow:

  • A block, entity, item, or packet may enter through an adapter reference.
  • The service contract should describe ECHO meaning.
  • Player interfaces should receive normalized data.
  • Save data should avoid storing runtime objects directly.

Failure Behavior

Contracts should define what happens when:

  • A dependency is missing.
  • A provider returns no data.
  • A feature is unsupported on the selected runtime target.
  • A runtime reference cannot be resolved.
  • Player progression does not allow full information yet.

Prefer explicit states such as unknown, unavailable, locked, unsupported-runtime, or missing-provider. Silent failure makes diagnostics and launcher repair harder.

Implementation Checklist

  • The contract ID is stable and names behavior.
  • Inputs and outputs are documented.
  • Runtime-specific references are isolated.
  • Required and optional dependencies are declared in echo.mod.json.
  • Optional integrations degrade cleanly.
  • Registration happens in an addon entrypoint, not hidden static initialization.
  • Logs include enough context for support.
  • The contract has at least one provider and one consumer path.
  • Related docs link to the contract.

Common Mistakes

  • Passing Minecraft or NeoForge objects through every layer because it is convenient.
  • Letting a UI module own data that belongs to a gameplay module.
  • Returning null without a reason code or fallback state.
  • Registering providers in hidden static initializers.
  • Adding a contract without documenting lifecycle and data ownership.
  • Copying first-party implementation packages into public SDK examples.

Related Docs

Next Step

Once the contract exists, decide whether it needs network synchronization in Networking or persistent ownership in Data Storage.