Files
unity-ingame-logging/Editor/GameLogViewerWindow.cs
Sebastian Bularca 2872300873 added code from unity
2026-04-06 20:45:22 +02:00

339 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Jovian.SaveSystem;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
namespace Jovian.InGameLogging.Editor {
public class GameLogViewerWindow : EditorWindow {
private static readonly Regex richTextRegex = new(@"<[^>]+>", RegexOptions.Compiled);
private Vector2 slotListScroll;
private Vector2 logEntriesScroll;
private List<SessionGroup> sessions = new();
private SaveSlotEntry selectedSlot;
private List<LogEntry> loadedEntries = new();
private List<LogEntry> filteredEntries = new();
private List<string> channelNames = new();
private int selectedChannelIndex;
private string savePath;
private GUIStyle channelTagStyle;
private GUIStyle timeStyle;
private GUIStyle messageStyle;
private GUIStyle slotButtonStyle;
private GUIStyle noDataStyle;
private bool stylesInitialized;
[MenuItem("Jovian/In-Game Logging/Log Viewer")]
public static void ShowWindow() {
var window = GetWindow<GameLogViewerWindow>("In-Game Log Viewer");
window.minSize = new Vector2(500, 400);
window.Show();
}
private void OnEnable() {
ScanSaves();
}
private void InitStyles() {
if(stylesInitialized) {
return;
}
channelTagStyle = new GUIStyle(EditorStyles.miniLabel) {
fontStyle = FontStyle.Bold,
normal = { textColor = new Color(0.5f, 0.8f, 1f) },
fixedWidth = 120,
alignment = TextAnchor.MiddleLeft
};
timeStyle = new GUIStyle(EditorStyles.miniLabel) {
normal = { textColor = new Color(0.6f, 0.6f, 0.6f) },
fixedWidth = 70,
alignment = TextAnchor.MiddleRight
};
messageStyle = new GUIStyle(EditorStyles.label) {
wordWrap = false,
richText = false
};
slotButtonStyle = new GUIStyle(EditorStyles.toolbarButton) {
alignment = TextAnchor.MiddleLeft,
fixedHeight = 22
};
noDataStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel) {
fontSize = 12,
wordWrap = true
};
stylesInitialized = true;
}
private void OnGUI() {
InitStyles();
DrawToolbar();
if(sessions.Count == 0) {
EditorGUILayout.LabelField("No save files found.\nPlay the game and save to generate log data.",
noDataStyle, GUILayout.ExpandHeight(true));
return;
}
DrawSlotList();
DrawChannelFilter();
DrawLogEntries();
}
private void DrawToolbar() {
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.FlexibleSpace();
if(GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60))) {
ScanSaves();
}
EditorGUILayout.EndHorizontal();
}
private void DrawSlotList() {
EditorGUILayout.LabelField("Save Slots", EditorStyles.boldLabel);
slotListScroll = EditorGUILayout.BeginScrollView(slotListScroll, GUILayout.MaxHeight(200));
foreach(var session in sessions) {
session.foldout = EditorGUILayout.Foldout(session.foldout, session.displayName, true);
if(!session.foldout) {
continue;
}
EditorGUI.indentLevel++;
foreach(var slot in session.slots) {
var isSelected = selectedSlot != null && selectedSlot.filePath == slot.filePath;
var style = new GUIStyle(slotButtonStyle);
if(isSelected) {
style.fontStyle = FontStyle.Bold;
}
if(GUILayout.Button($" {slot.displayName}", style)) {
SelectSlot(slot);
}
}
EditorGUI.indentLevel--;
}
EditorGUILayout.EndScrollView();
DrawSeparator();
}
private void DrawChannelFilter() {
if(loadedEntries.Count == 0) {
return;
}
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Channel filter:", GUILayout.Width(90));
var newIndex = EditorGUILayout.Popup(selectedChannelIndex, channelNames.ToArray());
if(newIndex != selectedChannelIndex) {
selectedChannelIndex = newIndex;
ApplyChannelFilter();
}
EditorGUILayout.EndHorizontal();
}
private void DrawLogEntries() {
if(selectedSlot == null) {
EditorGUILayout.LabelField("Select a save slot to view its log entries.", noDataStyle, GUILayout.ExpandHeight(true));
return;
}
if(filteredEntries.Count == 0) {
EditorGUILayout.LabelField("No log entries in this save.", noDataStyle, GUILayout.ExpandHeight(true));
return;
}
EditorGUILayout.LabelField($"{filteredEntries.Count} entries", EditorStyles.miniLabel);
logEntriesScroll = EditorGUILayout.BeginScrollView(logEntriesScroll, GUILayout.ExpandHeight(true));
foreach(var entry in filteredEntries) {
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"[{entry.channel}]", channelTagStyle);
EditorGUILayout.LabelField(FormatTime(entry.gameTime), timeStyle);
EditorGUILayout.LabelField(StripRichText(entry.message), messageStyle);
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.EndScrollView();
}
private void DrawSeparator() {
var rect = EditorGUILayout.GetControlRect(false, 1);
EditorGUI.DrawRect(rect, new Color(0.3f, 0.3f, 0.3f));
}
private void ScanSaves() {
sessions.Clear();
selectedSlot = null;
loadedEntries.Clear();
filteredEntries.Clear();
channelNames.Clear();
selectedChannelIndex = 0;
var settings = SaveSystemSettings.Load();
savePath = Path.Combine(Application.persistentDataPath, settings.saveDirectoryName);
var indexPath = Path.Combine(savePath, "index.json");
if(!File.Exists(indexPath)) {
return;
}
try {
var indexJson = File.ReadAllText(indexPath);
var index = JsonConvert.DeserializeObject<SaveIndex>(indexJson);
if(index?.sessions == null || index.slots == null) {
return;
}
var slotsBySession = new Dictionary<string, List<SaveSlotEntry>>();
foreach(var slot in index.slots) {
if(!slotsBySession.ContainsKey(slot.sessionId)) {
slotsBySession[slot.sessionId] = new List<SaveSlotEntry>();
}
slotsBySession[slot.sessionId].Add(new SaveSlotEntry {
sessionId = slot.sessionId,
displayName = slot.DisplayLabel + " " + DateTimeOffset.FromUnixTimeMilliseconds(slot.timestampUtc).LocalDateTime.ToString("HH:mm"),
filePath = slot.filePath,
timestampUtc = slot.timestampUtc
});
}
foreach(var session in index.sessions) {
if(!slotsBySession.TryGetValue(session.sessionId, out var slots)) {
continue;
}
slots.Sort((a, b) => b.timestampUtc.CompareTo(a.timestampUtc));
sessions.Add(new SessionGroup {
sessionId = session.sessionId,
displayName = $"Session ({DateTimeOffset.FromUnixTimeMilliseconds(session.lastSaveDateUtc).LocalDateTime:yyyy-MM-dd HH:mm})",
foldout = true,
slots = slots
});
}
sessions.Sort((a, b) => string.Compare(b.sessionId, a.sessionId, StringComparison.Ordinal));
}
catch(Exception e) {
Debug.LogError($"[GameLogViewer] Failed to read save index: {e.Message}");
}
}
private void SelectSlot(SaveSlotEntry slot) {
selectedSlot = slot;
loadedEntries.Clear();
filteredEntries.Clear();
channelNames.Clear();
channelNames.Add("All");
selectedChannelIndex = 0;
var fullPath = Path.Combine(savePath, slot.filePath);
if(!File.Exists(fullPath)) {
Debug.LogWarning($"[GameLogViewer] Save file not found: {fullPath}");
return;
}
try {
var bytes = File.ReadAllBytes(fullPath);
var serializer = new JsonSaveSerializer();
var envelope = serializer.Deserialize<SaveEnvelope>(bytes);
if(envelope?.payload == null) {
return;
}
// Navigate via JToken to avoid coupling to NoxSavedDataSet
var gameLogToken = envelope.payload["gameLogData"];
if(gameLogToken == null) {
return;
}
var entriesToken = gameLogToken["entries"];
if(entriesToken == null) {
return;
}
var logData = entriesToken.ToObject<List<LogEntry>>();
if(logData == null) {
return;
}
loadedEntries.AddRange(logData);
// Build channel list
var uniqueChannels = new HashSet<string>();
foreach(var entry in loadedEntries) {
var channelId = entry.channel.Id;
if(!string.IsNullOrEmpty(channelId) && uniqueChannels.Add(channelId)) {
channelNames.Add(channelId);
}
}
ApplyChannelFilter();
}
catch(Exception e) {
Debug.LogError($"[GameLogViewer] Failed to load save: {e.Message}");
}
}
private void ApplyChannelFilter() {
filteredEntries.Clear();
if(selectedChannelIndex == 0) {
filteredEntries.AddRange(loadedEntries);
}
else {
var channelId = channelNames[selectedChannelIndex];
foreach(var entry in loadedEntries) {
if(entry.channel.Id == channelId) {
filteredEntries.Add(entry);
}
}
}
}
private static string StripRichText(string text) {
return string.IsNullOrEmpty(text) ? string.Empty : richTextRegex.Replace(text, string.Empty);
}
private static string FormatTime(float gameTime) {
var minutes = (int)(gameTime / 60f);
var seconds = gameTime % 60f;
return $"{minutes}:{seconds:00.0}";
}
// Internal types for deserialization -- mirrors save system index structure
[Serializable]
private sealed class SaveIndex {
public List<SaveSessionInfo> sessions;
public List<SaveSlotInfo> slots;
}
private sealed class SessionGroup {
public string sessionId;
public string displayName;
public bool foldout;
public List<SaveSlotEntry> slots;
}
private sealed class SaveSlotEntry {
public string sessionId;
public string displayName;
public string filePath;
public long timestampUtc;
}
}
}