Skip to content

Commit

Permalink
docs(cardinal): update documentation to match current implementation (#…
Browse files Browse the repository at this point in the history
…819)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Scott Sunarto <[email protected]>
  • Loading branch information
devin-ai-integration[bot] and Scott Sunarto authored Dec 11, 2024
1 parent d73098f commit d7353c2
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 31 deletions.
44 changes: 36 additions & 8 deletions docs/cardinal/game/configuration/cardinal.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ TELEMETRY_TRACE_ENABLED = false

### BASE_SHARD_ROUTER_KEY

A secure authentication token used for routing game shards. It ensures proper routing within the Cardinal system.
A secure authentication token used to authenticate Cardinal with the EVM Base Shard gRPC private endpoint. This key ensures secure communication between Cardinal and the base shard services.
Router key must be length 64 and only contain alphanumerics.

**Example**
Expand Down Expand Up @@ -56,25 +56,54 @@ CARDINAL_LOG_PRETTY = true

### CARDINAL_NAMESPACE

A unique identifier for the Cardinal shard namespace. This ensures that different shards don’t interfere with each other and prevents signature replay attacks.
A unique identifier for the Cardinal shard namespace. This configuration is critical for security:

- Prevents signature replay attacks across different Cardinal instances
- Ensures unique transaction signatures per game instance

Each Cardinal instance must have a unique namespace to ensure that signatures cannot be replayed across different instances of the game. This is particularly important in production environments where multiple game instances may be running simultaneously.

**Example**
```
CARDINAL_NAMESPACE = 'defaultnamespace'
# Production namespace
CARDINAL_NAMESPACE = 'prod-game-v1'
# Development namespace
CARDINAL_NAMESPACE = 'dev-game-v1'
```

### CARDINAL_ROLLUP_ENABLED

Enables or disables rollup mode, where Cardinal sequences and recovers transactions on the base shard, default value is false.
Controls Cardinal's rollup mode, which affects transaction handling and state management:

- **When Enabled (true)**:
- Cardinal sequences and recovers transactions on the base shard
- Requires valid BASE_SHARD_SEQUENCER_ADDRESS
- Provides stronger consistency guarantees
- Suitable for production deployments

- **When Disabled (false)**:
- Processes transactions locally
- Useful for development and testing
- No sequencer dependency
- Default setting

**Example**
```
# Production setting
CARDINAL_ROLLUP_ENABLED = true
# Development setting
CARDINAL_ROLLUP_ENABLED = false
```

### REDIS_ADDRESS

The address of the Redis server, this parameter is unused if you are running cardinal using world cli v1.3.1, because world cli will force you to use local redis container
The address of the Redis server used for storing game state. When using world cli v1.3.1 or later, this setting is automatically managed:

- **Local Development**: world cli creates and manages a local Redis container
- **Production**: Configure this for your production Redis instance
- **Testing**: Uses an in-memory Redis instance

**Example**
```
Expand All @@ -83,20 +112,19 @@ REDIS_ADDRESS = 'localhost:6379'

### REDIS_PASSWORD

The password for the Redis server. Leave empty for no password.
The password for the Redis server. Leave empty for no password.
Make sure to set this in production to secure your Redis instance.

**Example**
```
REDIS_PASSWORD = ''
```


### TELEMETRY_TRACE_ENABLED

Enables trace collection, allowing for continuous application monitoring and tracing.

**Example**
```
TELEMETRY_TRACE_ENABLED = false
```
```
102 changes: 93 additions & 9 deletions docs/cardinal/game/system/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: 'How to create and register a system'

<Warning>
If you are unfamiliar with Entity Component System (ECS), we recommend reading [Introduction to ECS](/cardinal/ecs) before proceeding.

If you are unfamiliar with the concept of game loop and tick, we recommend reading [Loop-driven Runtime](/cardinal/loop) before proceeding.
</Warning>

Expand All @@ -18,7 +18,7 @@ func System(worldCtx cardinal.WorldContext) error
```

**Example:**
- A `RegenSystem` that increments the current health of all entities that have the `Health` component.
- A `RegenSystem` that increments the current health of all entities that have the `Health` component.
- An `AttackSystem` that handles the `AttackPlayerMsg` message and reduces the health of the target player.

---
Expand All @@ -28,16 +28,37 @@ Before we implement our systems, there are high-level concepts that you need to

### Systems are always executed once per tick

In Cardinal, systems are executed once per tick regardless of whether there are user message/transactions.
In Cardinal, systems are executed once per tick regardless of whether there are user message/transactions.

<Tip>
If you are coming from EVM development background, you might notice that this behavior is in stark contrast to how smart contracts work.

In smart contracts, game state can only be updated when a transaction calls a function of the contract. In Cardinal, game state is updated via systems at every tick regardless of whether there are transactions.

This makes it easy to implement state updates (e.g. regeneration, gravity, etc.) that need to consistently happen at every time step/interval which EVM smart contracts are not able to do seamlessly.
</Tip>

### System Execution Order
Systems are executed sequentially in the order they are registered. This order is critical for game logic as it determines the sequence of state updates within each tick. For example:

```go
// Systems execute in this order:
// 1. InputSystem processes player inputs
// 2. MovementSystem updates positions
// 3. CollisionSystem checks for collisions
// 4. CombatSystem resolves combat
cardinal.RegisterSystems(w,
system.InputSystem,
system.MovementSystem,
system.CollisionSystem,
system.CombatSystem,
)
```

<Warning>
Carefully consider the dependencies between your systems when determining their execution order. For example, collision detection should typically run after movement updates.
</Warning>

### All game state must be stored in components
As a general rule of thumb, systems should not store any game state in global variables as it will not be persisted. Systems should only store & read game state to & from components.

Expand All @@ -55,7 +76,7 @@ You can easily create a new system and register it to the world by following the

```go /system/regen.go
package system

func RegenSystem(worldCtx cardinal.WorldContext) error {
// ...
return nil
Expand All @@ -77,10 +98,17 @@ You can easily create a new system and register it to the world by following the

// Register systems
// Each system executes sequentially in the order they are added.
// NOTE: You must register your systems here for it to be executed.
err := cardinal.RegisterSystems(w, system.RegenSystem)
// Systems should be registered in dependency order:
// 1. Input processing systems
// 2. Game logic systems
// 3. Output/Effect systems
err := cardinal.RegisterSystems(w,
system.InputSystem, // Process player inputs first
system.MovementSystem, // Update positions based on inputs
system.CombatSystem, // Resolve combat after movement
)
if err != nil {
log.Fatal().Err(err).Msg("failed to register system")
log.Fatal().Err(err).Msg("failed to register systems")
}

// ...
Expand Down Expand Up @@ -155,6 +183,62 @@ func RegenSystem(worldCtx cardinal.WorldContext) error {
}
```

### Error Handling Best Practices

When implementing systems, proper error handling is crucial for maintaining game stability and debugging:

1. **Component Operations**
```go
// Always check errors from component operations
health, err := cardinal.GetComponent[component.Health](worldCtx, id)
if err != nil {
// Log the error with context
log.Error().Err(err).
Str("component", "Health").
Uint64("entity", uint64(id)).
Msg("failed to get component")
return fmt.Errorf("failed to get Health component: %w", err)
}
```

2. **Message Handling**
```go
// Handle message processing errors gracefully
return cardinal.EachMessage[msg.AttackMsg, msg.AttackMsgReply](worldCtx,
func(attack cardinal.TxData[msg.AttackMsg]) (msg.AttackMsgReply, error) {
if err := validateAttack(attack); err != nil {
// Return meaningful error responses to clients
return msg.AttackMsgReply{
Success: false,
Error: "invalid attack parameters",
}, nil
}
// Process valid attack
// PLACEHOLDER: attack processing logic
},
)
```

3. **Entity Creation**
```go
// Proper error handling for entity creation
id, err := cardinal.Create(worldCtx,
component.Player{Name: "Player1"},
component.Health{Current: 100, Maximum: 100},
)
if err != nil {
return fmt.Errorf("failed to create player entity: %w", err)
}
```

<Warning>
Always wrap errors with context using `fmt.Errorf` and include relevant entity IDs and component names in error messages for easier debugging.
</Warning>

<Tip>
Use structured logging (e.g., zerolog) to include additional context in error logs, making it easier to diagnose issues in production.
</Tip>

### Handling Messages
<CodeGroup>
```go /system/attack.go
Expand Down Expand Up @@ -225,4 +309,4 @@ func queryTargetPlayer(worldCtx cardinal.WorldContext, targetNickname string) (c
return playerID, playerHealth, err
}
```
</CodeGroup>
</CodeGroup>
51 changes: 47 additions & 4 deletions docs/cardinal/game/world/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ This method has no parameters.
The WithReceiptHistorySize option specifies the number of ticks for which the World object retains receipts. For instance, at tick 40 with a receipt history size of 5, the World stores receipts from ticks 35 to 39. Upon reaching tick 41, it will hold receipts for ticks 36 to 40. If this option remains unset, it defaults to a history size of 10. Game clients can get receipts via the [/query/receipts/list](/cardinal/rest/query-receipts-list) endpoint. Nakama also uses this endpoint to transmit receipts to listening clients.

```go
func WithReceiptHistorySize(size int) Option
func WithReceiptHistorySize(size int) WorldOption
```

##### Parameters
Expand All @@ -91,6 +91,10 @@ func WithReceiptHistorySize(size int) Option
|-----------|----------|-------------------------------------------------------|
| size | int | The size of the receipt history to be set for World. |

<Warning>
Setting a very large receipt history size may impact memory usage and performance. Choose a size that balances your game's needs with resource constraints.
</Warning>

#### WithStoreManager

The `WithStoreManager` option overrides the default gamestate manager. The gamestate manager is responsible for storing entity and component information, and recovering those values after a world restart. A default manager will be created if this option is unset.
Expand Down Expand Up @@ -123,13 +127,24 @@ func WithTickChannel(ch <-chan time.Time) WorldOption
##### Example

```go
// option to make ticks happen every 500 milliseconds.
// Example 1: Set tick rate to 500ms (2 ticks per second)
opt := WithTickChannel(time.Tick(500*time.Millisecond))

// Example 2: Set tick rate to 50ms (20 ticks per second) for fast-paced games
opt := WithTickChannel(time.Tick(50*time.Millisecond))

// Example 3: Use a custom channel for manual tick control in tests
tickCh := make(chan time.Time)
opt := WithTickChannel(tickCh)
```

<Warning>
Choose your tick rate carefully based on your game's requirements. Higher tick rates provide smoother updates but require more processing power and network bandwidth.
</Warning>

#### WithTickDoneChannel

The `WithTickDone` option sets a channel that will receive the just-completed tick number each time a tick completes execution. All systems are guaranteed to have been called when a message appears on this channel. This is useful in tests (in conjunction with the WithTickChannel) to make sure your user-defined Systems have fully executed before checking expectations.
The `WithTickDoneChannel` option sets a channel that will receive the just-completed tick number each time a tick completes execution. All systems are guaranteed to have been called when a message appears on this channel. This is particularly useful in tests to ensure your systems have fully executed before checking expectations.

```go
func WithTickDoneChannel(ch chan<- uint64) WorldOption
Expand All @@ -141,7 +156,35 @@ func WithTickDoneChannel(ch chan<- uint64) WorldOption
|-----------|-----------------|------------------------------------------------------------|
| ch | `chan<- uint64` | The channel that will be notified at the end of each tick. |

## RegisterSystems
##### Example
```go
// Example usage in tests
func TestGameSystem(t *testing.T) {
tickCh := make(chan time.Time)
doneCh := make(chan uint64)

world, _ := cardinal.NewWorld(
WithTickChannel(tickCh),
WithTickDoneChannel(doneCh),
)

// Start game in a goroutine
go world.StartGame()

// Trigger a tick
tickCh <- time.Now()

// Wait for tick to complete
<-doneCh

// Now safe to check game state
// Your test assertions here...
}
```

<Tip>
The WithTickDoneChannel is essential for writing deterministic tests. Always wait for the done signal before making assertions about game state changes.
</Tip>

`RegisterSystems` registers one or more systems to the `World`. Systems are executed in the order of which they were added to the world.

Expand Down
Loading

0 comments on commit d7353c2

Please sign in to comment.