# In-Game Logging System Design

**Package:** `com.jovian.ingame-logging`
**Date:** 2026-04-05

## Purpose

An optimized, low-allocation in-game logging system for displaying gameplay messages (roll values, damage, events) to the player. Injectable via constructor DI, not static. Saves/loads with the Jovian save system.

## Core Data Types

### LogChannel (readonly struct)

Zero-allocation value type with string-based identity for serialization.

```csharp
[Serializable]
public readonly struct LogChannel : IEquatable<LogChannel> {
    [SerializeField] readonly string id;
    public string Id => id;
    public LogChannel(string id) => this.id = id;

    // Built-in channels
    public static readonly LogChannel Combat = new("Combat");
    public static readonly LogChannel CharacterCreation = new("CharacterCreation");
    public static readonly LogChannel World = new("World");
    public static readonly LogChannel General = new("General");

    // IEquatable, ==, !=, GetHashCode via id
}
```

Game code can define additional channels: `new LogChannel("Crafting")`.
Serialized via Newtonsoft `JsonConverter` that reads/writes the `id` string.

### LogEntry (readonly struct)

```csharp
[Serializable]
public readonly struct LogEntry {
    public readonly string message;    // supports TMP rich text tags
    public readonly LogChannel channel;
    public readonly float gameTime;    // Time.time when logged
}
```

## Architecture

```
Game Code (CharacterCreationView, CombatSystem, etc.)
  │  var log = new InGameLogger(store, LogChannel.Combat);
  │  log.Log("Rolled 18 for Might");
  │  log.Log("<color=#FFD700>Critical Hit!</color> 24 damage");
  ▼
InGameLogger (readonly struct, per-caller facade)
  │  Holds: IGameLogStore reference + LogChannel
  │  log.Log(msg) → store.Add(channel, msg)
  │  log.Log(msg, color) → store.Add(channel, colored msg)
  ▼
IGameLogStore (singleton service, created in EntryPoint)
  │  Ring buffer with configurable max capacity (default 500)
  │  Add(channel, message), Clear(), Clear(channel)
  │  GetEntries(), GetEntries(channel)
  │  event Action<LogEntry> OnEntryAdded
  │  event Action OnCleared
  │  SaveLog(ISaveSystem, sessionId), LoadLog(ISaveSystem, sessionId)
  ▼
GameLogView (MonoBehaviour on UI prefab)
    ScrollRect + VerticalLayoutGroup
    Object pool of LogEntryView instances
    Subscribes to IGameLogStore.OnEntryAdded / OnCleared
    Optional channel filter (show one or all channels)
    Auto-scroll to bottom, pause on manual scroll-up
```

## InGameLogger (Facade Struct)

```csharp
public readonly struct InGameLogger {
    readonly IGameLogStore store;
    readonly LogChannel channel;

    public InGameLogger(IGameLogStore store, LogChannel channel) {
        this.store = store;
        this.channel = channel;
    }

    public void Log(string message) => store.Add(channel, message);
    public void Log(string message, string hexColor)
        => store.Add(channel, $"<color={hexColor}>{message}</color>");
}
```

- No allocations beyond the message string itself
- Each caller (view, system) creates its own instance scoped to a channel
- Injected via `IGameLogStore` passed through constructor DI

## IGameLogStore (Service Interface)

```csharp
public interface IGameLogStore {
    int Count { get; }
    int Capacity { get; }

    void Add(LogChannel channel, string message);
    void Clear();
    void Clear(LogChannel channel);

    ReadOnlySpan<LogEntry> GetEntries();
    // Filtered access for UI
    int GetEntries(LogChannel channel, List<LogEntry> results);

    event Action<LogEntry> OnEntryAdded;
    event Action OnCleared;

    // Save integration
    GameLogSaveData GetSaveData();
    void RestoreFromSaveData(GameLogSaveData data);
}
```

## GameLogStore (Implementation)

- **Ring buffer** backed by `LogEntry[]` array
- Configurable capacity (default 500), oldest entries evicted when full
- `Add()` writes to next slot, increments head pointer, fires `OnEntryAdded`
- `Clear()` resets head/count, fires `OnCleared`
- `Clear(LogChannel)` removes entries matching channel, compacts buffer

## Save Integration

**Dependency direction:** `com.jovian.ingame-logging` depends on `com.jovian.savesystem`.

```csharp
[Serializable]
public sealed class GameLogSaveData {
    public List<LogEntry> entries;  // all channels, in order
}
```

- `GetSaveData()` snapshots the ring buffer into a `GameLogSaveData`
- `RestoreFromSaveData()` rebuilds the ring buffer and notifies UI
- All channels saved together in one payload, each entry tagged with its `LogChannel`
- Game code calls save/load alongside its own persistence:
  - Save: `saveSystem.Save(logStore.GetSaveData())` in the same session
  - Load: `logStore.RestoreFromSaveData(saveSystem.Load<GameLogSaveData>(slot))`

## UI Design

### GameLogView (MonoBehaviour)

- Attached to a prefab with `ScrollRect` + `Content` (VerticalLayoutGroup)
- Holds reference to a `LogEntryView` prefab (for pooling)
- Subscribes to `IGameLogStore.OnEntryAdded` and `OnCleared`
- Object pool: pre-warms ~20 `LogEntryView` instances
- When pool is exhausted, recycles oldest visible entries
- Optional `LogChannel` filter field (null = show all)
- Auto-scrolls to bottom; pauses auto-scroll when user scrolls up manually

### LogEntryView (MonoBehaviour)

- Minimal: single `TMP_Text` serialized field reference
- `SetEntry(LogEntry entry)` sets text content
- TMP `richText = true` by default (supports `<color>`, `<b>`, `<size>`, etc.)

## Package Structure

```
Packages/com.jovian.ingame-logging/
├── package.json                          # com.jovian.ingame-logging, deps: savesystem, TMP
├── Runtime/
│   ├── Jovian.InGameLogging.asmdef
│   ├── LogChannel.cs
│   ├── LogEntry.cs
│   ├── InGameLogger.cs
│   ├── IGameLogStore.cs
│   ├── GameLogStore.cs
│   ├── GameLogSaveData.cs
│   ├── LogChannelJsonConverter.cs
│   └── UI/
│       ├── GameLogView.cs
│       └── LogEntryView.cs
└── Editor/
    └── Jovian.InGameLogging.Editor.asmdef
```

### Dependencies

- `com.jovian.savesystem` (for ISaveSystem)
- `com.unity.textmeshpro` (for TMP_Text)
- `com.unity.nuget.newtonsoft-json` (for LogChannel serialization)

### Assembly References

- `Jovian.InGameLogging` references: `Jovian.SaveSystem`, `Unity.TextMeshPro`, `Newtonsoft.Json`
- `Jovian.InGameLogging.Editor` references: `Jovian.InGameLogging`

## Integration Points (Game Project)

1. **EntryPoint.cs**: Create `GameLogStore` instance, pass to game states that need logging
2. **Game state constructors**: Receive `IGameLogStore`, create `InGameLogger` structs per channel
3. **UI prefab**: Instantiate `GameLogView` prefab, call `Initialize(IGameLogStore)`
4. **Save/Load**: Call `GetSaveData()` / `RestoreFromSaveData()` alongside existing save flow

## Design Decisions

| Decision | Rationale |
|----------|-----------|
| Readonly struct for LogChannel | Zero-alloc, value semantics, serializable via string id |
| Readonly struct for InGameLogger | Per-caller facade with no heap allocation |
| Ring buffer over List | Fixed capacity, no resizing, O(1) add, oldest eviction built-in |
| Object pool for UI | Eliminates instantiation after warmup, O(visible) rendering cost |
| Separate save file via save system | Logging depends on save system, not the other way around |
| TMP rich text passthrough | No parsing overhead; callers compose rich text directly |
| Simple Clear() API | Package stays decoupled from game-specific events |
