# Jovian In-Game Logging

A low-allocation in-game logging system for Unity with channel-based filtering, pooled UI, and save system integration. This is a player-facing game log (combat feed, event history), not a debug logger.

## Requirements

- Unity 2022.3 or later
- Dependencies:
  - `com.unity.textmeshpro` 3.0.6+
  - `com.unity.nuget.newtonsoft-json` 3.2.1+
  - `com.jovian.savesystem` 0.1.0+

## Quick Start

### 1. Create the store

`GameLogStore` is the central service that holds log entries in a ring buffer. Create it once and pass it via constructor injection.

```csharp
// Default capacity of 500 entries
var logStore = new GameLogStore();

// Or specify a custom capacity
var logStore = new GameLogStore(capacity: 1000);
```

### 2. Create a logger

`InGameLogger` is a lightweight facade bound to a specific channel. Create one per system or class that needs to write log entries.

```csharp
var combatLogger = new InGameLogger(logStore, LogChannel.Combat);
var worldLogger = new InGameLogger(logStore, LogChannel.World);
```

### 3. Log messages

```csharp
combatLogger.Log("Kael strikes the skeleton for 12 damage.");
combatLogger.Log("Critical hit!", "#FF4444");
```

### 4. Set up the UI

Add a `GameLogView` component to a GameObject with a `ScrollRect`. Assign the `ScrollRect`, a content `RectTransform`, and a `LogEntryView` prefab in the Inspector. Then initialize from code:

```csharp
// Show all channels
gameLogView.Initialize(logStore);

// Or filter to a single channel
gameLogView.Initialize(logStore, LogChannel.Combat);
```

The view uses object pooling internally. It auto-scrolls to the bottom as new entries arrive and pauses auto-scroll when the player scrolls up manually.

## Prefab Setup

The UI requires two prefabs: a **LogEntryView prefab** (the individual log row) and a **GameLogView prefab** (the scrollable container).

### LogEntryView prefab

1. Create a new GameObject, rename it `LogEntry`
2. Add a `LayoutElement` component. Set `Preferred Height` to your desired row height (e.g. 24). Enable `Flexible Width` so it stretches to the scroll content width
3. Add a child GameObject with a `TextMeshPro - Text (UI)` component
   - Set `Overflow` to `Ellipsis` or `Truncate` (prevents text from spilling outside the row)
   - Anchor and stretch it to fill the parent (`Left: 0, Right: 0, Top: 0, Bottom: 0`)
   - Set font, font size, and color to match your game's UI style
   - `Rich Text` should remain enabled (default)
4. Add the `LogEntryView` component to the root `LogEntry` GameObject
5. Drag the child `TMP_Text` into the `LogEntryView.messageText` field
6. Save as a prefab

### GameLogView prefab

1. Create a new UI GameObject with a `ScrollRect` component
   - Disable `Horizontal` scrolling, keep `Vertical` enabled
   - Set `Movement Type` to `Clamped` or `Elastic` as preferred
   - Set `Scroll Sensitivity` to a comfortable value (e.g. 20)
2. Add a child `Content` GameObject as the scroll content area
   - Add a `RectTransform` anchored top-left, pivot `(0, 1)`, with a `VerticalLayoutGroup`:
     - `Child Alignment`: Upper Left
     - `Child Force Expand Width`: true, `Height`: false
     - `Spacing`: 2 (gap between log rows)
     - `Padding`: set as needed
   - Add a `ContentSizeFitter` with `Vertical Fit` set to `Preferred Size` (so it grows as entries are added)
   - Assign this as the `ScrollRect.Content`
3. Optionally add a `Scrollbar` child for the vertical scrollbar, and assign it to `ScrollRect.Vertical Scrollbar`
4. Optionally add a `Mask` or `RectMask2D` on the scroll viewport to clip entries outside the visible area
5. Add the `GameLogView` component to the root GameObject
6. Assign the fields in the Inspector:
   - `scrollRect`: the `ScrollRect` component
   - `content`: the `Content` child's `RectTransform`
   - `entryPrefab`: your `LogEntryView` prefab
   - `poolSize`: number of pre-instantiated entries (default 20, increase for taller views)

### Initialization from code

After instantiating or finding the `GameLogView` in the scene:

```csharp
var gameLogView = Object.FindFirstObjectByType<GameLogView>();
gameLogView.Initialize(logStore);

// Or filtered to a single channel:
gameLogView.Initialize(logStore, LogChannel.Combat);
```

## Log Channels

### Built-in channels

| Channel | Usage |
|---------|-------|
| `LogChannel.Combat` | Combat events, damage, abilities |
| `LogChannel.CharacterCreation` | Character creation flow |
| `LogChannel.World` | World events, exploration |
| `LogChannel.General` | General-purpose messages |

### Custom channels

`LogChannel` is a readonly struct keyed by a string ID. Define custom channels as static fields:

```csharp
public static class MyLogChannels {
    public static readonly LogChannel Trading = new("Trading");
    public static readonly LogChannel Dialogue = new("Dialogue");
}
```

Then use them like any built-in channel:

```csharp
var tradeLogger = new InGameLogger(logStore, MyLogChannels.Trading);
tradeLogger.Log("Sold Iron Sword for 50 gold.");
```

## Enable / Disable Channels

Each logger can be toggled on or off. When disabled, calls to `Log` are silently dropped (no allocation, no entry added). All channels are enabled by default.

```csharp
var combatLog = new InGameLogger(logStore, LogChannel.Combat);

combatLog.Disable();
combatLog.Log("This message is silently dropped.");

combatLog.Enable();
combatLog.Log("Back online.");

if(combatLog.IsEnabled) {
    // check state
}
```

The state lives in the `IGameLogStore`, so disabling a channel from one `InGameLogger` instance disables it for all instances using the same store and channel. You can also call the store directly:

```csharp
logStore.DisableChannel(LogChannel.World);
logStore.EnableChannel(LogChannel.World);
bool enabled = logStore.IsChannelEnabled(LogChannel.World);
```

## Rich Text

Log messages support TextMeshPro rich text tags. The `InGameLogger.Log(message, hexColor)` overload wraps the message in a `<color>` tag automatically:

```csharp
logger.Log("Poisoned!", "#00FF00");
// Stored as: <color=#00FF00>Poisoned!</color>
```

You can also embed TMP tags directly in the message string:

```csharp
logger.Log("Gained <b>+5</b> <color=#FFD700>experience</color>.");
```

## Save / Load Integration

`GameLogStore` supports serializing its contents for use with a save system.

### Save

```csharp
GameLogSaveData saveData = logStore.GetSaveData();
// Serialize saveData into your save file
```

### Load

```csharp
// Deserialize saveData from your save file
logStore.RestoreFromSaveData(saveData);
```

`RestoreFromSaveData` replaces the current buffer contents. If the save data contains more entries than the store's capacity, only the most recent entries are kept. The `OnCleared` event fires after restoration so the UI can rebuild.

### JSON serialization

`LogChannel` is serialized as a plain string via the included `LogChannelJsonConverter` (Newtonsoft.Json). The converter is applied via attribute on `LogEntry.channel`, so no manual converter registration is needed.

## API Reference

### LogChannel (readonly struct)

- `LogChannel(string id)` -- constructor
- `string Id` -- the channel identifier
- Static fields: `Combat`, `CharacterCreation`, `World`, `General`
- Implements `IEquatable<LogChannel>`, ordinal string comparison

### LogEntry (readonly struct)

- `string message` -- the log message (may contain TMP rich text)
- `LogChannel channel` -- the channel this entry belongs to
- `float gameTime` -- `Time.time` when the entry was added

### InGameLogger (readonly struct)

- `InGameLogger(IGameLogStore store, LogChannel channel)` -- constructor
- `void Log(string message)` -- add an entry to the store
- `void Log(string message, string hexColor)` -- add a color-wrapped entry
- `void Enable()` -- enable this logger's channel
- `void Disable()` -- disable this logger's channel (Log calls are silently dropped)
- `bool IsEnabled` -- whether this logger's channel is currently enabled

### IGameLogStore / GameLogStore

- `GameLogStore(int capacity = 500)` -- constructor with ring buffer size
- `int Count` -- current number of entries
- `int Capacity` -- maximum entries before oldest are overwritten
- `void Add(LogChannel channel, string message)` -- add an entry (silently dropped if channel is disabled)
- `void EnableChannel(LogChannel channel)` -- enable a channel
- `void DisableChannel(LogChannel channel)` -- disable a channel
- `bool IsChannelEnabled(LogChannel channel)` -- check if a channel is enabled
- `void Clear()` -- remove all entries
- `void Clear(LogChannel channel)` -- remove entries for a specific channel
- `ReadOnlySpan<LogEntry> GetEntries()` -- all entries in chronological order
- `int GetEntries(LogChannel channel, List<LogEntry> results)` -- filtered entries, returns count
- `event Action<LogEntry> OnEntryAdded` -- fires after each new entry
- `event Action OnCleared` -- fires after `Clear()` or `RestoreFromSaveData()`
- `GameLogSaveData GetSaveData()` -- snapshot for serialization
- `void RestoreFromSaveData(GameLogSaveData data)` -- replace contents from save data

### GameLogSaveData

- `List<LogEntry> entries` -- serializable entry list

### GameLogView (MonoBehaviour) -- `Jovian.InGameLogging.UI`

- `void Initialize(IGameLogStore store, LogChannel? channelFilter = null)` -- bind to a store, optionally filtering to one channel
- Requires: `ScrollRect`, content `RectTransform`, and `LogEntryView` prefab assigned in Inspector

### LogEntryView (MonoBehaviour) -- `Jovian.InGameLogging.UI`

- `void SetEntry(in LogEntry entry)` -- display an entry
- `void ClearEntry()` -- reset the text
- Requires: `TMP_Text` reference assigned in Inspector

### LogChannelJsonConverter

- Newtonsoft.Json `JsonConverter<LogChannel>` -- serializes `LogChannel` as its string ID
