feat: Implement runtime API cache store and related configurations
- Added RuntimeApiCacheStore for managing API cache entries in memory. - Introduced StorageTarget enum to define various storage targets. - Created StoredCacheEntry record to encapsulate cache entries. - Defined SyncState enum to represent the synchronization state of cache entries. - Added ControlPlaneConfigurationSource enum for different configuration sources. - Developed ControlPlaneRuntimeConfig class for runtime configuration management. - Implemented ControlPlaneStartupConfigResolver for resolving configuration from various sources. - Created ControlPlaneStartupSettings to hold startup configuration values. - Added ResolvedControlPlaneStartupConfig record to encapsulate resolved configuration. - Implemented tests for batch client and cache client functionalities. - Added tests for ControlPlaneStartupConfigResolver to validate configuration resolution logic.
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# API Response Storage
|
||||
|
||||
The cache layer stores API results and controls when data should be pulled from or pushed to the control plane.
|
||||
|
||||
Use it when multiple plugins need shared ownership of:
|
||||
|
||||
- API response freshness
|
||||
- stale fallback behavior
|
||||
- player-persistent API data
|
||||
- dirty local data that should be pushed later
|
||||
|
||||
Entry point:
|
||||
|
||||
```java
|
||||
ApiDataCacheClient cache = NetworkPlugin.client().cache();
|
||||
```
|
||||
|
||||
## Storage Targets
|
||||
|
||||
Every cached call explicitly chooses a target.
|
||||
|
||||
```java
|
||||
StorageTarget.RUNTIME_SHARED
|
||||
StorageTarget.PLAYER
|
||||
StorageTarget.ASSET_STORE
|
||||
```
|
||||
|
||||
`RUNTIME_SHARED`
|
||||
|
||||
- process-local in-memory cache
|
||||
- best for server profile, config, catalog reads, active announcements
|
||||
- does not survive restart
|
||||
|
||||
`PLAYER`
|
||||
|
||||
- stored on `NetworkApiDataComponent`
|
||||
- requires a `PlayerCacheContext`
|
||||
- persists with Hytale player component data
|
||||
|
||||
`ASSET_STORE`
|
||||
|
||||
- requires an `AssetStoreCacheAdapter`
|
||||
- intended for plugins that can map API data into concrete Hytale asset types
|
||||
- core-network owns freshness metadata, the adapter owns asset conversion
|
||||
|
||||
## Register A Cached Call
|
||||
|
||||
```java
|
||||
CachedApiCall<ServerProfile> serverProfileCall = CachedApiCall
|
||||
.builder(ServerProfile.class)
|
||||
.id("server-profile")
|
||||
.method("GET")
|
||||
.uri("/api/v1/server/profile")
|
||||
.storageTarget(StorageTarget.RUNTIME_SHARED)
|
||||
.maxAge(Duration.ofSeconds(30))
|
||||
.build();
|
||||
|
||||
cache.register(serverProfileCall);
|
||||
```
|
||||
|
||||
Read cached or pull if stale:
|
||||
|
||||
```java
|
||||
CompletableFuture<ServerProfile> profile =
|
||||
cache.getOrPull(serverProfileCall, CacheContext.shared());
|
||||
```
|
||||
|
||||
Force refresh:
|
||||
|
||||
```java
|
||||
CompletableFuture<ServerProfile> profile =
|
||||
cache.forcePull(serverProfileCall, CacheContext.shared());
|
||||
```
|
||||
|
||||
## Player Storage
|
||||
|
||||
Player storage requires explicit context. Core-network does not infer player identity from the API URI.
|
||||
|
||||
```java
|
||||
PlayerCacheContext context = new PlayerCacheContext(playerId, playerRef, entityStore);
|
||||
|
||||
CachedApiCall<PlayerSessionProfile> sessionProfile = CachedApiCall
|
||||
.builder(PlayerSessionProfile.class)
|
||||
.id("player-session-profile")
|
||||
.method("GET")
|
||||
.uri(() -> "/api/v1/players/" + playerId + "/session-profile")
|
||||
.storageTarget(StorageTarget.PLAYER)
|
||||
.maxAge(Duration.ofMinutes(5))
|
||||
.build();
|
||||
|
||||
CompletableFuture<PlayerSessionProfile> profile =
|
||||
cache.getOrPull(sessionProfile, context);
|
||||
```
|
||||
|
||||
`NetworkPlugin.setup()` registers `NetworkApiDataComponent` with the entity store registry.
|
||||
|
||||
## Freshness Policies
|
||||
|
||||
Auto-refresh when older than max age:
|
||||
|
||||
```java
|
||||
.maxAge(Duration.ofMinutes(5))
|
||||
```
|
||||
|
||||
Never auto-refresh:
|
||||
|
||||
```java
|
||||
.neverRefresh()
|
||||
```
|
||||
|
||||
`forcePull(...)` always bypasses freshness checks.
|
||||
|
||||
## Dirty Push Policies
|
||||
|
||||
Update local data and mark it dirty:
|
||||
|
||||
```java
|
||||
cache.updateLocal(call, context, value);
|
||||
```
|
||||
|
||||
Push immediately:
|
||||
|
||||
```java
|
||||
cache.forcePush(call, context);
|
||||
```
|
||||
|
||||
Push after dirty TTL:
|
||||
|
||||
```java
|
||||
CachedApiCall<MyModel> call = CachedApiCall
|
||||
.builder(MyModel.class)
|
||||
.id("my-writeback")
|
||||
.method("GET")
|
||||
.uri("/api/v1/some/read/path")
|
||||
.dirtyMaxAge(Duration.ofSeconds(10))
|
||||
.push(
|
||||
"POST",
|
||||
(context, value) -> "/api/v1/some/write/path",
|
||||
(context, value) -> value
|
||||
)
|
||||
.build();
|
||||
```
|
||||
|
||||
The plugin scheduler calls:
|
||||
|
||||
```java
|
||||
cache.flushDirty();
|
||||
```
|
||||
|
||||
Manual-only push:
|
||||
|
||||
```java
|
||||
.manualPushOnly()
|
||||
```
|
||||
|
||||
## Failed Sync Metadata
|
||||
|
||||
If refresh fails and an old value exists:
|
||||
|
||||
- stale value is returned
|
||||
- `failedSyncCount` increments
|
||||
- `lastError` is stored
|
||||
- `syncState` becomes `FAILED`
|
||||
|
||||
If refresh fails and no value exists:
|
||||
|
||||
- the future completes exceptionally
|
||||
|
||||
Read metadata:
|
||||
|
||||
```java
|
||||
Optional<ApiCacheMetadata> metadata =
|
||||
cache.metadata(call, context);
|
||||
```
|
||||
|
||||
Successful pull or push resets `failedSyncCount` to `0`.
|
||||
|
||||
## Asset Store Adapter
|
||||
|
||||
Asset-store-backed calls need a plugin adapter.
|
||||
|
||||
```java
|
||||
public final class MyAssetCacheAdapter implements AssetStoreCacheAdapter {
|
||||
@Override
|
||||
public Optional<ApiCacheEntry> read(ApiCacheKey key, CacheContext context) {
|
||||
// Read concrete Hytale asset and convert to ApiCacheEntry.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(ApiCacheKey key, ApiCacheEntry entry, CacheContext context) {
|
||||
// Convert entry payload into concrete asset and load/write through AssetStore.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use it like:
|
||||
|
||||
```java
|
||||
CachedApiCall<JsonObject> call = CachedApiCall
|
||||
.builder(JsonObject.class)
|
||||
.id("asset-backed-config")
|
||||
.method("GET")
|
||||
.uri("/api/v1/configs/active")
|
||||
.storageTarget(StorageTarget.ASSET_STORE)
|
||||
.assetStoreAdapter(new MyAssetCacheAdapter())
|
||||
.maxAge(Duration.ofMinutes(10))
|
||||
.build();
|
||||
```
|
||||
|
||||
## Batching Behavior
|
||||
|
||||
When `ControlPlaneClient.batchingEnabled()` is true, cache pulls and pushes use `BatchClient`.
|
||||
|
||||
When batching is disabled, cache operations call the HTTP transport directly.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Batched API Calls
|
||||
|
||||
Batching lets multiple plugins enqueue control-plane calls that are sent as one HTTP request to `/api/v1/batch`.
|
||||
|
||||
Use batching when:
|
||||
|
||||
- many plugins read data at the same time
|
||||
- looped tasks report state periodically
|
||||
- call latency can be handled asynchronously
|
||||
|
||||
Do not use batching when:
|
||||
|
||||
- the caller needs a blocking result immediately
|
||||
- the endpoint should bypass queueing for operational safety
|
||||
|
||||
## One-Off Calls
|
||||
|
||||
```java
|
||||
CompletableFuture<ServerProfile> future = NetworkPlugin.client()
|
||||
.batch()
|
||||
.get(ProfileEndpoint.VALUE, ProfileEndpoint.VALUE.uri(), ServerProfile.class);
|
||||
```
|
||||
|
||||
```java
|
||||
CompletableFuture<JsonObject> future = NetworkPlugin.client()
|
||||
.batch()
|
||||
.enqueueJson("GET", "/api/v1/server/profile", null);
|
||||
```
|
||||
|
||||
Queued calls complete when the batch loop flushes, or when code calls:
|
||||
|
||||
```java
|
||||
NetworkPlugin.client().batch().flush();
|
||||
```
|
||||
|
||||
## Looped Calls
|
||||
|
||||
```java
|
||||
LoopHandle handle = NetworkPlugin.client().batch().registerLoopedCall(
|
||||
LoopedApiCall.builder(ServerProfile.class)
|
||||
.id("server-profile-loop")
|
||||
.interval(Duration.ofSeconds(5))
|
||||
.method("GET")
|
||||
.uri("/api/v1/server/profile")
|
||||
.onSuccess(profile -> {
|
||||
// update local state
|
||||
})
|
||||
.onFailure(error -> {
|
||||
// log or mark degraded
|
||||
})
|
||||
.build()
|
||||
);
|
||||
```
|
||||
|
||||
Cancel when no longer needed:
|
||||
|
||||
```java
|
||||
handle.cancel();
|
||||
```
|
||||
|
||||
## Batch Endpoint Contract
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"requests": [
|
||||
{
|
||||
"id": "generated-call-id",
|
||||
"method": "GET",
|
||||
"uri": "/api/v1/server/profile",
|
||||
"body": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"responses": [
|
||||
{
|
||||
"id": "generated-call-id",
|
||||
"status": 200,
|
||||
"body": { "data": {} },
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each item completes independently. A failed item does not fail unrelated futures.
|
||||
|
||||
## Defaults
|
||||
|
||||
- endpoint: `/api/v1/batch`
|
||||
- flush interval: `250ms`
|
||||
- max batch size: `100`
|
||||
- batching enabled: `true`
|
||||
|
||||
Configure with `ControlPlaneClient.builder()`.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Control Plane Client
|
||||
|
||||
`ControlPlaneClient` is the main public entry point for plugins.
|
||||
|
||||
```java
|
||||
ControlPlaneClient client = NetworkPlugin.client();
|
||||
```
|
||||
|
||||
## Public Clients
|
||||
|
||||
```java
|
||||
client.server(); // ServerClient
|
||||
client.presence(); // PresenceClient
|
||||
client.players(); // PlayersClient
|
||||
client.batch(); // BatchClient
|
||||
client.cache(); // ApiDataCacheClient
|
||||
```
|
||||
|
||||
## Handwritten Domain Clients
|
||||
|
||||
`ServerClient`
|
||||
|
||||
```java
|
||||
ServerProfile profile = client.server().currentProfile();
|
||||
```
|
||||
|
||||
`PresenceClient`
|
||||
|
||||
```java
|
||||
client.presence().register(instanceId, registrationPayload);
|
||||
client.presence().heartbeat(instanceId, HeartbeatPayload.running(12, 150));
|
||||
client.presence().shutdown(instanceId, "restart");
|
||||
```
|
||||
|
||||
`PlayersClient`
|
||||
|
||||
```java
|
||||
PlayerSessionProfile profile = client.players().getSessionProfile(playerId);
|
||||
List<PlayerEntitlement> entitlements = client.players().getEntitlements(playerId);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Transport errors use checked `ControlPlaneException` subclasses:
|
||||
|
||||
- `AuthException` for `401` and `403`
|
||||
- `NotFoundException` for `404`
|
||||
- `ValidationException` for `422`
|
||||
- `RemoteException` for `5xx`
|
||||
- `ControlPlaneException` for other transport or parsing failures
|
||||
|
||||
Example:
|
||||
|
||||
```java
|
||||
try {
|
||||
ServerProfile profile = client.server().currentProfile();
|
||||
} catch (AuthException e) {
|
||||
// token is missing, invalid, inactive, or lacks ability
|
||||
} catch (ControlPlaneException e) {
|
||||
// generic control-plane failure
|
||||
}
|
||||
```
|
||||
|
||||
## Endpoint Descriptors
|
||||
|
||||
Generated endpoint descriptors are available under:
|
||||
|
||||
```text
|
||||
net.kewwbec.network.generated.api.v1
|
||||
```
|
||||
|
||||
Use them for stable route metadata and URI templates. Resolve path variables with `PathBuilder`.
|
||||
|
||||
```java
|
||||
String uri = PathBuilder.resolve(
|
||||
SessionProfileEndpoint.VALUE.uri(),
|
||||
"player",
|
||||
playerId
|
||||
);
|
||||
```
|
||||
|
||||
Do not edit generated descriptors manually. Update routes in `core-control-plane`, then run the SDK sync command from that repo.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Generated SDK
|
||||
|
||||
The generated SDK is a Java view of the control-plane route contract.
|
||||
|
||||
Generated files live under:
|
||||
|
||||
```text
|
||||
src/main/java/net/kewwbec/network/generated/api/v1
|
||||
```
|
||||
|
||||
These files include:
|
||||
|
||||
- route names
|
||||
- allowed HTTP methods
|
||||
- URI templates
|
||||
- required abilities
|
||||
- middleware metadata
|
||||
- request field descriptors
|
||||
- response type descriptors
|
||||
|
||||
## Usage
|
||||
|
||||
Import the generated root:
|
||||
|
||||
```java
|
||||
import net.kewwbec.network.generated.api.v1.ControlPlaneApiV1;
|
||||
```
|
||||
|
||||
Use domain APIs to inspect route descriptors:
|
||||
|
||||
```java
|
||||
ControlPlaneApiV1 api = ControlPlaneApiV1.INSTANCE;
|
||||
String version = api.apiVersion();
|
||||
int endpointCount = api.endpointCount();
|
||||
```
|
||||
|
||||
Use route endpoint constants with handwritten clients or batching:
|
||||
|
||||
```java
|
||||
String uri = PathBuilder.resolve(
|
||||
SessionProfileEndpoint.VALUE.uri(),
|
||||
"player",
|
||||
playerId
|
||||
);
|
||||
```
|
||||
|
||||
## Regeneration
|
||||
|
||||
The source of truth is `../core-control-plane`.
|
||||
|
||||
Run from `core-control-plane`:
|
||||
|
||||
```bash
|
||||
php artisan control-plane:sync-network-sdk --dry-run
|
||||
php artisan control-plane:sync-network-sdk
|
||||
```
|
||||
|
||||
Review generated churn carefully before committing. Broad generated diffs can happen if `core-control-plane` has unrelated route changes.
|
||||
|
||||
## Rules
|
||||
|
||||
- Do not manually patch generated Java.
|
||||
- Fix Laravel routes, requests, resources, or the generator upstream.
|
||||
- Re-run the sync after route changes.
|
||||
- Keep handwritten Java under `net.kewwbec.network`, outside `generated`.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Getting Started
|
||||
|
||||
## Build
|
||||
|
||||
From `core-network`:
|
||||
|
||||
```bat
|
||||
gradlew.bat compileJava
|
||||
gradlew.bat test
|
||||
gradlew.bat shadowJar
|
||||
```
|
||||
|
||||
`compileJava` and `test` are the fastest checks for SDK/runtime changes. `shadowJar` builds the plugin jar.
|
||||
|
||||
## Configure The Shared Client
|
||||
|
||||
`core-network` auto-configures `NetworkPlugin.client()` during plugin setup when startup configuration is present.
|
||||
|
||||
Recommended production setup uses environment variables:
|
||||
|
||||
```powershell
|
||||
$env:KWB_CONTROL_PLANE_BASE_URL = "https://control-plane.internal"
|
||||
$env:KWB_CONTROL_PLANE_TOKEN = "server-issued-token"
|
||||
```
|
||||
|
||||
Supported optional environment variables:
|
||||
|
||||
```text
|
||||
KWB_CONTROL_PLANE_BATCHING_ENABLED=true
|
||||
KWB_CONTROL_PLANE_BATCH_FLUSH_INTERVAL_MS=250
|
||||
KWB_CONTROL_PLANE_BATCH_MAX_SIZE=100
|
||||
KWB_CONTROL_PLANE_BATCH_ENDPOINT=/api/v1/batch
|
||||
```
|
||||
|
||||
Launcher-controlled startup can use JVM properties:
|
||||
|
||||
```text
|
||||
-Dkweebec.controlPlane.baseUrl=https://control-plane.internal
|
||||
-Dkweebec.controlPlane.token=server-issued-token
|
||||
-Dkweebec.controlPlane.batching.enabled=true
|
||||
-Dkweebec.controlPlane.batch.flushIntervalMs=250
|
||||
-Dkweebec.controlPlane.batch.maxSize=100
|
||||
-Dkweebec.controlPlane.batch.endpoint=/api/v1/batch
|
||||
```
|
||||
|
||||
For small local deployments, `core-network` creates `control-plane.json` in the plugin data directory. Fill in the placeholders:
|
||||
|
||||
```json
|
||||
{
|
||||
"BaseUrl": "https://control-plane.internal",
|
||||
"Token": "local-dev-token",
|
||||
"BatchingEnabled": true,
|
||||
"BatchFlushIntervalMs": 250,
|
||||
"MaxBatchSize": 100,
|
||||
"BatchEndpoint": "/api/v1/batch"
|
||||
}
|
||||
```
|
||||
|
||||
Precedence is environment variables, then JVM properties, then `control-plane.json`.
|
||||
|
||||
Manual setup still works and overrides auto-config:
|
||||
|
||||
```java
|
||||
NetworkPlugin.configure(
|
||||
"https://control-plane.internal",
|
||||
System.getenv("KWB_CONTROL_PLANE_TOKEN")
|
||||
);
|
||||
```
|
||||
|
||||
Dependent plugins then use:
|
||||
|
||||
```java
|
||||
ControlPlaneClient client = NetworkPlugin.client();
|
||||
```
|
||||
|
||||
You can inspect the source that configured the shared client:
|
||||
|
||||
```java
|
||||
NetworkPlugin.configurationSource();
|
||||
```
|
||||
|
||||
## Dependency Setup
|
||||
|
||||
Dependent jars should declare `core-network` as a plugin dependency in their manifest:
|
||||
|
||||
```json
|
||||
{
|
||||
"Dependencies": {
|
||||
"core-network": "*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Direct Calls
|
||||
|
||||
```java
|
||||
ServerProfile profile = NetworkPlugin.client()
|
||||
.server()
|
||||
.currentProfile();
|
||||
```
|
||||
|
||||
```java
|
||||
PlayerSessionProfile profile = NetworkPlugin.client()
|
||||
.players()
|
||||
.getSessionProfile(playerId);
|
||||
```
|
||||
|
||||
```java
|
||||
NetworkPlugin.client()
|
||||
.presence()
|
||||
.heartbeat(instanceId, HeartbeatPayload.running(currentPlayers, maxPlayers));
|
||||
```
|
||||
|
||||
## Runtime Scheduling
|
||||
|
||||
`NetworkPlugin` starts a scheduled loop when the Hytale plugin starts. The loop:
|
||||
|
||||
- flushes due batched API calls
|
||||
- flushes dirty cached API entries
|
||||
|
||||
Default batch cadence is `250ms`.
|
||||
|
||||
Builder overrides:
|
||||
|
||||
```java
|
||||
ControlPlaneClient client = ControlPlaneClient.builder()
|
||||
.baseUrl("https://control-plane.internal")
|
||||
.token(token)
|
||||
.batchingEnabled(true)
|
||||
.batchFlushInterval(Duration.ofMillis(250))
|
||||
.maxBatchSize(100)
|
||||
.batchEndpoint("/api/v1/batch")
|
||||
.build();
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
# Core Network Wiki
|
||||
|
||||
`core-network` is the Hytale plugin-side runtime for Kweebec Network control-plane access.
|
||||
|
||||
It provides:
|
||||
|
||||
- a shared `ControlPlaneClient`
|
||||
- handwritten convenience clients for common server/player operations
|
||||
- generated API endpoint descriptors from `core-control-plane`
|
||||
- batched HTTP calls through `/api/v1/batch`
|
||||
- API response storage policies for runtime, player, and asset-store-backed data
|
||||
|
||||
## Pages
|
||||
|
||||
- [Getting Started](Getting-Started.md)
|
||||
- [Control Plane Client](Control-Plane-Client.md)
|
||||
- [Batched API Calls](Batched-API-Calls.md)
|
||||
- [API Response Storage](API-Response-Storage.md)
|
||||
- [Generated SDK](Generated-SDK.md)
|
||||
- [Troubleshooting](Troubleshooting.md)
|
||||
|
||||
## Package Map
|
||||
|
||||
```text
|
||||
net.kewwbec.network
|
||||
ControlPlaneClient.java shared client entry point
|
||||
NetworkPlugin.java Hytale plugin entry point and scheduler hook
|
||||
client/ handwritten domain clients
|
||||
http/ low-level JSON HTTP transport
|
||||
batch/ one-off and looped batched calls
|
||||
cache/ cached API data and storage policies
|
||||
generated/api/v1/ generated endpoint descriptors
|
||||
models/ handwritten response/request models
|
||||
```
|
||||
|
||||
## Design Rules
|
||||
|
||||
- Do not edit `net.kewwbec.network.generated` manually.
|
||||
- Add handwritten runtime behavior outside `generated`.
|
||||
- Treat `core-control-plane` as the API owner.
|
||||
- Use batching for repeated or concurrent calls that can tolerate async completion.
|
||||
- Use cache policies when plugin code needs shared ownership of API freshness or push timing.
|
||||
@@ -0,0 +1,116 @@
|
||||
# Troubleshooting
|
||||
|
||||
## `NetworkPlugin has not been configured`
|
||||
|
||||
No startup source produced both a control-plane base URL and token before another plugin requested `NetworkPlugin.client()`.
|
||||
|
||||
Fix with one of:
|
||||
|
||||
- environment variables: `KWB_CONTROL_PLANE_BASE_URL` and `KWB_CONTROL_PLANE_TOKEN`
|
||||
- JVM properties: `kweebec.controlPlane.baseUrl` and `kweebec.controlPlane.token`
|
||||
- local plugin config: `control-plane.json`
|
||||
- explicit startup call: `NetworkPlugin.configure(baseUrl, token)`
|
||||
|
||||
Check which source configured the client:
|
||||
|
||||
```java
|
||||
NetworkPlugin.configurationSource();
|
||||
```
|
||||
|
||||
`Optional.empty()` means no source configured the shared client.
|
||||
|
||||
## Startup Config Precedence
|
||||
|
||||
Startup values are resolved in this order:
|
||||
|
||||
1. environment variables
|
||||
2. JVM system properties
|
||||
3. `control-plane.json`
|
||||
|
||||
Environment variables and JVM properties override only the fields they set. Missing fields can still come from a lower-priority source.
|
||||
|
||||
## Config File Token Warning
|
||||
|
||||
`control-plane.json` is a short-term local deployment fallback. Prefer environment variables for production or control-plane-provisioned servers.
|
||||
|
||||
JVM `-D` properties are supported, but some host tooling can expose process arguments. Avoid logging launcher commands that contain tokens.
|
||||
|
||||
## Auth Failures
|
||||
|
||||
`AuthException` maps to HTTP `401` or `403`.
|
||||
|
||||
Common causes:
|
||||
|
||||
- missing token
|
||||
- expired or invalid token
|
||||
- inactive service account
|
||||
- token missing required ability
|
||||
- server-instance token trying to access a different instance route
|
||||
|
||||
## Batch Futures Never Complete
|
||||
|
||||
Likely causes:
|
||||
|
||||
- batching scheduler has not started
|
||||
- `NetworkPlugin` is not loaded as the plugin main class
|
||||
- code enqueued calls but never called `batch().flush()` in tests
|
||||
- control plane does not expose `/api/v1/batch`
|
||||
|
||||
In tests, call:
|
||||
|
||||
```java
|
||||
client.batch().flush();
|
||||
```
|
||||
|
||||
## Cached Reads Keep Returning Stale Data
|
||||
|
||||
This can be expected behavior. If a refresh fails and an old value exists, cache APIs return the stale value and increment failed sync metadata.
|
||||
|
||||
Check:
|
||||
|
||||
```java
|
||||
cache.metadata(call, context)
|
||||
```
|
||||
|
||||
Look at:
|
||||
|
||||
- `failedSyncCount`
|
||||
- `lastError`
|
||||
- `syncState`
|
||||
|
||||
## `PLAYER cache calls require PlayerCacheContext`
|
||||
|
||||
Player storage is explicit. Use:
|
||||
|
||||
```java
|
||||
new PlayerCacheContext(playerId, playerRef, entityStore)
|
||||
```
|
||||
|
||||
Core-network will not infer player identity from `/players/{player}` URIs.
|
||||
|
||||
## `NetworkApiDataComponent has not been registered`
|
||||
|
||||
The player cache component is registered in `NetworkPlugin.setup()`.
|
||||
|
||||
This error usually means:
|
||||
|
||||
- tests are using player storage without plugin setup
|
||||
- `NetworkPlugin` is not the active Hytale plugin main class
|
||||
- code is accessing player cache before component registration
|
||||
|
||||
## Asset Store Cache Does Nothing
|
||||
|
||||
`ASSET_STORE` storage requires a plugin-provided `AssetStoreCacheAdapter`.
|
||||
|
||||
Core-network does not create a generic Hytale asset store because Hytale assets require concrete asset classes, codecs, keys, and load/write behavior.
|
||||
|
||||
## Manifest Rewrites During Gradle Tasks
|
||||
|
||||
`processResources` depends on `updatePluginManifest`, which rewrites `src/main/resources/manifest.json` from Gradle properties.
|
||||
|
||||
If the manifest changes unexpectedly after tests/builds, check:
|
||||
|
||||
- `includes_pack` in `gradle.properties`
|
||||
- `version` in `gradle.properties`
|
||||
|
||||
Avoid committing unrelated manifest churn.
|
||||
Reference in New Issue
Block a user