added code from unity
This commit is contained in:
338
Editor/GameLogViewerWindow.cs
Normal file
338
Editor/GameLogViewerWindow.cs
Normal file
@@ -0,0 +1,338 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Editor/GameLogViewerWindow.cs.meta
Normal file
2
Editor/GameLogViewerWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f19bfe069d21db41a998154aa0876fb
|
||||
19
Editor/Jovian.InGameLogging.Editor.asmdef
Normal file
19
Editor/Jovian.InGameLogging.Editor.asmdef
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "Jovian.InGameLogging.Editor",
|
||||
"rootNamespace": "Jovian.InGameLogging.Editor",
|
||||
"references": [
|
||||
"Jovian.InGameLogging",
|
||||
"Jovian.SaveSystem"
|
||||
],
|
||||
"includePlatforms": ["Editor"],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"Newtonsoft.Json.dll"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Editor/Jovian.InGameLogging.Editor.asmdef.meta
Normal file
7
Editor/Jovian.InGameLogging.Editor.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 48c945ba5ea83b144b5bbf4eaf33fe29
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user