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_servicehello_content_addon:registry_serviceechoterminal:terminal/surfaceecholens: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
nullwithout 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.