# In-Game Logging System Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Build `com.jovian.ingame-logging`, a low-allocation in-game logging package with pooled UI, channel-based filtering, and save system integration.

**Architecture:** Injectable `IGameLogStore` service with ring buffer storage. `InGameLogger` readonly struct facades scoped to a `LogChannel`. Pooled `GameLogView` MonoBehaviour for ScrollRect UI. Save data as `GameLogSaveData` serializable type, persisted as a field in the game's `NoxSavedDataSet`.

**Tech Stack:** Unity 6 / C# 9, TextMeshPro, Newtonsoft.Json, Jovian SaveSystem

---

### Task 1: Package scaffold

**Files:**
- Create: `Packages/com.jovian.ingame-logging/package.json`
- Create: `Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef`
- Create: `Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef`

**Step 1: Create package.json**

```json
{
    "name": "com.jovian.ingame-logging",
    "version": "0.1.0",
    "displayName": "Jovian In-Game Logging",
    "description": "An optimized, low-allocation in-game logging system with pooled UI, channel-based filtering, and save system integration.",
    "unity": "2022.3",
    "dependencies": {
        "com.unity.textmeshpro": "3.0.6",
        "com.unity.nuget.newtonsoft-json": "3.2.1",
        "com.jovian.savesystem": "0.1.0"
    },
    "keywords": [
        "logging",
        "ui",
        "ingame"
    ],
    "author": {
        "name": "Jovian"
    }
}
```

**Step 2: Create Runtime asmdef**

```json
{
    "name": "Jovian.InGameLogging",
    "rootNamespace": "Jovian.InGameLogging",
    "references": [
        "Unity.TextMeshPro",
        "Jovian.SaveSystem"
    ],
    "includePlatforms": [],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": true,
    "precompiledReferences": [
        "Newtonsoft.Json.dll"
    ],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}
```

**Step 3: Create Editor asmdef**

```json
{
    "name": "Jovian.InGameLogging.Editor",
    "rootNamespace": "Jovian.InGameLogging.Editor",
    "references": [
        "Jovian.InGameLogging"
    ],
    "includePlatforms": ["Editor"],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}
```

**Step 4: Commit**

```bash
git add Packages/com.jovian.ingame-logging/
git commit -m "feat: scaffold com.jovian.ingame-logging package"
```

---

### Task 2: LogChannel readonly struct

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs`

**Step 1: Implement LogChannel**

```csharp
using System;
using UnityEngine;

namespace Jovian.InGameLogging {
    [Serializable]
    public readonly struct LogChannel : IEquatable<LogChannel> {
        [SerializeField] readonly string id;

        public string Id => id;

        public LogChannel(string id) {
            this.id = id;
        }

        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");

        public bool Equals(LogChannel other) => string.Equals(id, other.id, StringComparison.Ordinal);
        public override bool Equals(object obj) => obj is LogChannel other && Equals(other);
        public override int GetHashCode() => id != null ? id.GetHashCode() : 0;
        public override string ToString() => id ?? string.Empty;

        public static bool operator ==(LogChannel left, LogChannel right) => left.Equals(right);
        public static bool operator !=(LogChannel left, LogChannel right) => !left.Equals(right);
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs
git commit -m "feat: add LogChannel readonly struct with built-in channels"
```

---

### Task 3: LogChannelJsonConverter

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs`

**Step 1: Implement Newtonsoft converter**

```csharp
using System;
using Newtonsoft.Json;

namespace Jovian.InGameLogging {
    public sealed class LogChannelJsonConverter : JsonConverter<LogChannel> {
        public override void WriteJson(JsonWriter writer, LogChannel value, JsonSerializer serializer) {
            writer.WriteValue(value.Id);
        }

        public override LogChannel ReadJson(JsonReader reader, Type objectType, LogChannel existingValue, bool hasExistingValue, JsonSerializer serializer) {
            var id = reader.Value as string;
            return new LogChannel(id);
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs
git commit -m "feat: add Newtonsoft JSON converter for LogChannel"
```

---

### Task 4: LogEntry readonly struct

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs`

**Step 1: Implement LogEntry**

```csharp
using System;
using Newtonsoft.Json;

namespace Jovian.InGameLogging {
    [Serializable]
    public readonly struct LogEntry {
        public readonly string message;

        [JsonConverter(typeof(LogChannelJsonConverter))]
        public readonly LogChannel channel;

        public readonly float gameTime;

        public LogEntry(string message, LogChannel channel, float gameTime) {
            this.message = message;
            this.channel = channel;
            this.gameTime = gameTime;
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs
git commit -m "feat: add LogEntry readonly struct"
```

---

### Task 5: GameLogSaveData

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs`

**Step 1: Implement serializable save container**

```csharp
using System;
using System.Collections.Generic;

namespace Jovian.InGameLogging {
    [Serializable]
    public sealed class GameLogSaveData {
        public List<LogEntry> entries;

        public GameLogSaveData() {
            entries = new List<LogEntry>();
        }

        public GameLogSaveData(List<LogEntry> entries) {
            this.entries = entries;
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs
git commit -m "feat: add GameLogSaveData serializable container"
```

---

### Task 6: IGameLogStore interface

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs`

**Step 1: Define the store interface**

```csharp
using System;
using System.Collections.Generic;

namespace Jovian.InGameLogging {
    public interface IGameLogStore {
        int Count { get; }
        int Capacity { get; }

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

        ReadOnlySpan<LogEntry> GetEntries();
        int GetEntries(LogChannel channel, List<LogEntry> results);

        event Action<LogEntry> OnEntryAdded;
        event Action OnCleared;

        GameLogSaveData GetSaveData();
        void RestoreFromSaveData(GameLogSaveData data);
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs
git commit -m "feat: add IGameLogStore interface"
```

---

### Task 7: GameLogStore ring buffer implementation

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs`

**Step 1: Implement the ring buffer store**

```csharp
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Jovian.InGameLogging {
    public sealed class GameLogStore : IGameLogStore {
        readonly LogEntry[] buffer;
        int head;
        int count;

        public int Count => count;
        public int Capacity => buffer.Length;

        public event Action<LogEntry> OnEntryAdded;
        public event Action OnCleared;

        public GameLogStore(int capacity = 500) {
            buffer = new LogEntry[capacity];
            head = 0;
            count = 0;
        }

        public void Add(LogChannel channel, string message) {
            var entry = new LogEntry(message, channel, Time.time);
            buffer[head] = entry;
            head = (head + 1) % buffer.Length;
            if(count < buffer.Length) {
                count++;
            }
            OnEntryAdded?.Invoke(entry);
        }

        public void Clear() {
            head = 0;
            count = 0;
            OnCleared?.Invoke();
        }

        public void Clear(LogChannel channel) {
            // Compact: copy non-matching entries into a temp span, then rebuild
            var kept = new List<LogEntry>(count);
            var entries = GetEntries();
            for(int i = 0; i < entries.Length; i++) {
                if(entries[i].channel != channel) {
                    kept.Add(entries[i]);
                }
            }

            head = 0;
            count = 0;
            for(int i = 0; i < kept.Count; i++) {
                buffer[i] = kept[i];
                count++;
            }
            head = count % buffer.Length;
            OnCleared?.Invoke();
        }

        public ReadOnlySpan<LogEntry> GetEntries() {
            if(count < buffer.Length) {
                return new ReadOnlySpan<LogEntry>(buffer, 0, count);
            }
            // Buffer is full and wrapping - need to return in chronological order
            var result = new LogEntry[count];
            int start = head; // head points to the oldest entry when full
            for(int i = 0; i < count; i++) {
                result[i] = buffer[(start + i) % buffer.Length];
            }
            return result;
        }

        public int GetEntries(LogChannel channel, List<LogEntry> results) {
            results.Clear();
            var entries = GetEntries();
            for(int i = 0; i < entries.Length; i++) {
                if(entries[i].channel == channel) {
                    results.Add(entries[i]);
                }
            }
            return results.Count;
        }

        public GameLogSaveData GetSaveData() {
            var entries = GetEntries();
            var list = new List<LogEntry>(entries.Length);
            for(int i = 0; i < entries.Length; i++) {
                list.Add(entries[i]);
            }
            return new GameLogSaveData(list);
        }

        public void RestoreFromSaveData(GameLogSaveData data) {
            head = 0;
            count = 0;
            if(data?.entries == null) {
                OnCleared?.Invoke();
                return;
            }
            // Only restore up to capacity, taking the most recent entries
            int startIndex = Math.Max(0, data.entries.Count - buffer.Length);
            for(int i = startIndex; i < data.entries.Count; i++) {
                buffer[count] = data.entries[i];
                count++;
            }
            head = count % buffer.Length;
            OnCleared?.Invoke(); // Signal UI to rebuild from scratch
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs
git commit -m "feat: add GameLogStore ring buffer implementation"
```

---

### Task 8: InGameLogger facade struct

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs`

**Step 1: Implement the per-caller facade**

```csharp
using System.Runtime.CompilerServices;

namespace Jovian.InGameLogging {
    public readonly struct InGameLogger {
        readonly IGameLogStore store;
        readonly LogChannel channel;

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

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public void Log(string message) {
            store.Add(channel, message);
        }

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

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs
git commit -m "feat: add InGameLogger readonly struct facade"
```

---

### Task 9: LogEntryView MonoBehaviour

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs`

**Step 1: Implement the minimal UI entry component**

```csharp
using TMPro;
using UnityEngine;

namespace Jovian.InGameLogging.UI {
    public class LogEntryView : MonoBehaviour {
        [SerializeField] TMP_Text messageText;

        public void SetEntry(in LogEntry entry) {
            messageText.text = entry.message;
        }

        public void ClearEntry() {
            messageText.text = string.Empty;
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs
git commit -m "feat: add LogEntryView MonoBehaviour for pooled UI entries"
```

---

### Task 10: GameLogView MonoBehaviour with object pooling

**Files:**
- Create: `Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs`

**Step 1: Implement the scrollable log view with object pool**

```csharp
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

namespace Jovian.InGameLogging.UI {
    public class GameLogView : MonoBehaviour {
        [SerializeField] ScrollRect scrollRect;
        [SerializeField] RectTransform content;
        [SerializeField] LogEntryView entryPrefab;
        [SerializeField] int poolSize = 20;

        IGameLogStore store;
        LogChannel? channelFilter;
        bool autoScroll = true;

        readonly List<LogEntryView> activeEntries = new();
        readonly Stack<LogEntryView> pool = new();

        public void Initialize(IGameLogStore store, LogChannel? channelFilter = null) {
            this.store = store;
            this.channelFilter = channelFilter;

            WarmPool();
            store.OnEntryAdded += HandleEntryAdded;
            store.OnCleared += HandleCleared;
            scrollRect.onValueChanged.AddListener(HandleScrollChanged);

            // Populate existing entries
            RebuildFromStore();
        }

        void OnDestroy() {
            if(store != null) {
                store.OnEntryAdded -= HandleEntryAdded;
                store.OnCleared -= HandleCleared;
            }
        }

        void WarmPool() {
            for(int i = 0; i < poolSize; i++) {
                var entry = Instantiate(entryPrefab, content);
                entry.gameObject.SetActive(false);
                pool.Push(entry);
            }
        }

        LogEntryView GetFromPool() {
            LogEntryView entry;
            if(pool.Count > 0) {
                entry = pool.Pop();
            }
            else {
                // Recycle oldest active entry
                entry = activeEntries[0];
                activeEntries.RemoveAt(0);
            }
            entry.gameObject.SetActive(true);
            entry.transform.SetAsLastSibling();
            return entry;
        }

        void ReturnToPool(LogEntryView entry) {
            entry.ClearEntry();
            entry.gameObject.SetActive(false);
            pool.Push(entry);
        }

        void HandleEntryAdded(LogEntry entry) {
            if(channelFilter.HasValue && entry.channel != channelFilter.Value) {
                return;
            }

            var view = GetFromPool();
            view.SetEntry(in entry);
            activeEntries.Add(view);

            if(autoScroll) {
                Canvas.ForceUpdateCanvases();
                scrollRect.verticalNormalizedPosition = 0f;
            }
        }

        void HandleCleared() {
            for(int i = activeEntries.Count - 1; i >= 0; i--) {
                ReturnToPool(activeEntries[i]);
            }
            activeEntries.Clear();

            // If store still has entries (channel-specific clear), rebuild
            if(store.Count > 0) {
                RebuildFromStore();
            }
        }

        void RebuildFromStore() {
            var entries = store.GetEntries();
            for(int i = 0; i < entries.Length; i++) {
                if(channelFilter.HasValue && entries[i].channel != channelFilter.Value) {
                    continue;
                }
                var view = GetFromPool();
                view.SetEntry(in entries[i]);
                activeEntries.Add(view);
            }

            if(autoScroll) {
                Canvas.ForceUpdateCanvases();
                scrollRect.verticalNormalizedPosition = 0f;
            }
        }

        void HandleScrollChanged(Vector2 position) {
            // Pause auto-scroll when user scrolls up, resume when at bottom
            autoScroll = position.y <= 0.01f;
        }
    }
}
```

**Step 2: Commit**

```bash
git add Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs
git commit -m "feat: add GameLogView with object pooling and scroll management"
```

---

### Task 11: Generate Unity .meta files

Unity requires `.meta` files for every file and folder. These will be auto-generated by Unity when the editor imports the package. However, to make the package work without opening Unity, we need to ensure the directory structure is correct.

**Step 1: Create placeholder to ensure Editor directory exists**

Create an empty `Editor/.gitkeep` or let Unity generate metas on next editor open. Since this project builds through Unity Editor, the meta files will be generated automatically on next load.

**Step 2: Commit all remaining files**

```bash
git add Packages/com.jovian.ingame-logging/
git commit -m "feat: complete com.jovian.ingame-logging package"
```

---

### Task 12: Game integration - Add GameLogSaveData to NoxSavedDataSet

**Files:**
- Modify: `Assets/Code/GameState/NoxSaveData.cs` (add `GameLogSaveData` field to `NoxSavedDataSet`)

**Step 1: Add the field**

Add to `NoxSavedDataSet` class at `Assets/Code/GameState/NoxSaveData.cs:50`:

```csharp
using Jovian.InGameLogging;  // add to imports

// Inside NoxSavedDataSet class, add:
public GameLogSaveData gameLogData;
```

**Step 2: Commit**

```bash
git add Assets/Code/GameState/NoxSaveData.cs
git commit -m "feat: add GameLogSaveData field to NoxSavedDataSet"
```

---

### Task 13: Game integration - Wire GameLogStore in EntryPoint

**Files:**
- Modify: `Assets/Code/Core/EntryPoint.cs`

**Step 1: Create GameLogStore and pass it to game states**

In `EntryPoint.CreateApplicationStates()`, after the character systems creation (~line 109), add:

```csharp
using Jovian.InGameLogging;  // add to imports

// After characterSystems creation:
var gameLogStore = new GameLogStore(500);
```

Then pass `gameLogStore` as `IGameLogStore` to the game state constructors that need it. The exact constructors depend on which states need logging - at minimum `GameModeGameState` for combat/adventure logging.

**Step 2: Commit**

```bash
git add Assets/Code/Core/EntryPoint.cs
git commit -m "feat: wire GameLogStore in EntryPoint"
```

---

### Task 14: Game integration - Save/Load log data

**Files:**
- Modify: wherever `NoxSavedDataSet` is populated before save (look for `saveSystem.Save` calls)
- Modify: `Assets/Code/GameState/NoxSaveData.cs` (`RestoreSavedData` method)

**Step 1: Populate log data before save**

Wherever the game creates `NoxSavedDataSet` before saving, add:

```csharp
savedDataSet.gameLogData = gameLogStore.GetSaveData();
```

**Step 2: Restore log data after load**

In `NoxSaveData.RestoreSavedData()`, after loading save data (~line 35), add:

```csharp
// Pass gameLogStore as parameter to RestoreSavedData, then:
if(saveData.gameLogData != null) {
    gameLogStore.RestoreFromSaveData(saveData.gameLogData);
}
```

**Step 3: Commit**

```bash
git add Assets/Code/GameState/NoxSaveData.cs
git commit -m "feat: integrate log save/load with game persistence"
```

---

## File Summary

| File | Type | Purpose |
|------|------|---------|
| `Packages/com.jovian.ingame-logging/package.json` | Create | Package manifest |
| `Packages/com.jovian.ingame-logging/Runtime/Jovian.InGameLogging.asmdef` | Create | Runtime assembly |
| `Packages/com.jovian.ingame-logging/Editor/Jovian.InGameLogging.Editor.asmdef` | Create | Editor assembly |
| `Packages/com.jovian.ingame-logging/Runtime/LogChannel.cs` | Create | Channel type (readonly struct) |
| `Packages/com.jovian.ingame-logging/Runtime/LogChannelJsonConverter.cs` | Create | Newtonsoft serialization |
| `Packages/com.jovian.ingame-logging/Runtime/LogEntry.cs` | Create | Entry type (readonly struct) |
| `Packages/com.jovian.ingame-logging/Runtime/GameLogSaveData.cs` | Create | Serializable save container |
| `Packages/com.jovian.ingame-logging/Runtime/IGameLogStore.cs` | Create | Store interface |
| `Packages/com.jovian.ingame-logging/Runtime/GameLogStore.cs` | Create | Ring buffer implementation |
| `Packages/com.jovian.ingame-logging/Runtime/InGameLogger.cs` | Create | Per-caller facade struct |
| `Packages/com.jovian.ingame-logging/Runtime/UI/LogEntryView.cs` | Create | Entry UI component |
| `Packages/com.jovian.ingame-logging/Runtime/UI/GameLogView.cs` | Create | Scrollable log view with pool |
| `Assets/Code/GameState/NoxSaveData.cs` | Modify | Add GameLogSaveData field + restore |
| `Assets/Code/Core/EntryPoint.cs` | Modify | Create and wire GameLogStore |
