forked from Shardstone/trail-into-darkness
added portraits and others
This commit is contained in:
@@ -17,7 +17,8 @@ namespace Jovian.InGameLogging {
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Log(string message, string hexColor) {
|
||||
store.Add(channel, $"<color={hexColor}>{message}</color>");
|
||||
var prefix = hexColor.Length > 0 && hexColor[0] == '#' ? "" : "#";
|
||||
store.Add(channel, $"<color={prefix}{hexColor}>{message}</color>");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"Newtonsoft.Json.dll"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
|
||||
351
Packages/com.jovian.savesystem/Editor/SaveDataViewerWindow.cs
Normal file
351
Packages/com.jovian.savesystem/Editor/SaveDataViewerWindow.cs
Normal file
@@ -0,0 +1,351 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.SaveSystem.Editor {
|
||||
public class SaveDataViewerWindow : EditorWindow {
|
||||
private Vector2 slotListScroll;
|
||||
private Vector2 dataScroll;
|
||||
|
||||
private List<SessionGroup> sessions = new();
|
||||
private SlotEntry selectedSlot;
|
||||
private string savePath;
|
||||
|
||||
// Parsed data for the selected slot
|
||||
private SaveEnvelope loadedEnvelope;
|
||||
private List<PayloadField> payloadFields = new();
|
||||
|
||||
private GUIStyle labelStyle;
|
||||
private GUIStyle valueStyle;
|
||||
private GUIStyle headerStyle;
|
||||
private GUIStyle slotButtonStyle;
|
||||
private GUIStyle noDataStyle;
|
||||
private GUIStyle metaKeyStyle;
|
||||
private GUIStyle metaValueStyle;
|
||||
private bool stylesInitialized;
|
||||
|
||||
[MenuItem("Jovian/Save System/Save Data Viewer")]
|
||||
public static void ShowWindow() {
|
||||
var window = GetWindow<SaveDataViewerWindow>("Save Data Viewer");
|
||||
window.minSize = new Vector2(550, 450);
|
||||
window.Show();
|
||||
}
|
||||
|
||||
private void OnEnable() {
|
||||
ScanSaves();
|
||||
}
|
||||
|
||||
private void InitStyles() {
|
||||
if(stylesInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
headerStyle = new GUIStyle(EditorStyles.boldLabel) {
|
||||
fontSize = 12
|
||||
};
|
||||
|
||||
labelStyle = new GUIStyle(EditorStyles.label) {
|
||||
fontStyle = FontStyle.Bold,
|
||||
normal = { textColor = new Color(0.55f, 0.8f, 1f) }
|
||||
};
|
||||
|
||||
valueStyle = new GUIStyle(EditorStyles.label) {
|
||||
wordWrap = true
|
||||
};
|
||||
|
||||
slotButtonStyle = new GUIStyle(EditorStyles.toolbarButton) {
|
||||
alignment = TextAnchor.MiddleLeft,
|
||||
fixedHeight = 22
|
||||
};
|
||||
|
||||
noDataStyle = new GUIStyle(EditorStyles.centeredGreyMiniLabel) {
|
||||
fontSize = 12,
|
||||
wordWrap = true
|
||||
};
|
||||
|
||||
metaKeyStyle = new GUIStyle(EditorStyles.miniLabel) {
|
||||
fontStyle = FontStyle.Bold,
|
||||
fixedWidth = 100
|
||||
};
|
||||
|
||||
metaValueStyle = new GUIStyle(EditorStyles.miniLabel);
|
||||
|
||||
stylesInitialized = true;
|
||||
}
|
||||
|
||||
private void OnGUI() {
|
||||
InitStyles();
|
||||
DrawToolbar();
|
||||
|
||||
if(sessions.Count == 0) {
|
||||
EditorGUILayout.LabelField("No save files found.\nPlay the game and save to generate data.",
|
||||
noDataStyle, GUILayout.ExpandHeight(true));
|
||||
return;
|
||||
}
|
||||
|
||||
DrawSlotList();
|
||||
DrawSaveData();
|
||||
}
|
||||
|
||||
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", headerStyle);
|
||||
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)) {
|
||||
LoadSlot(slot);
|
||||
}
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
DrawSeparator();
|
||||
}
|
||||
|
||||
private void DrawSaveData() {
|
||||
if(selectedSlot == null) {
|
||||
EditorGUILayout.LabelField("Select a save slot to view its data.", noDataStyle, GUILayout.ExpandHeight(true));
|
||||
return;
|
||||
}
|
||||
|
||||
if(loadedEnvelope == null) {
|
||||
EditorGUILayout.LabelField("Failed to load save data.", noDataStyle, GUILayout.ExpandHeight(true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Envelope metadata
|
||||
EditorGUILayout.LabelField("Envelope", headerStyle);
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Version:", metaKeyStyle);
|
||||
EditorGUILayout.LabelField(loadedEnvelope.version.ToString(), metaValueStyle);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Slot Type:", metaKeyStyle);
|
||||
EditorGUILayout.LabelField(loadedEnvelope.slotType.ToString(), metaValueStyle);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField("Timestamp:", metaKeyStyle);
|
||||
var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(loadedEnvelope.timestampUtc).LocalDateTime;
|
||||
EditorGUILayout.LabelField(timestamp.ToString("yyyy-MM-dd HH:mm:ss"), metaValueStyle);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
DrawSeparator();
|
||||
EditorGUILayout.Space(4);
|
||||
EditorGUILayout.LabelField("Payload", headerStyle);
|
||||
|
||||
if(payloadFields.Count == 0) {
|
||||
EditorGUILayout.LabelField("(empty payload)", noDataStyle);
|
||||
return;
|
||||
}
|
||||
|
||||
dataScroll = EditorGUILayout.BeginScrollView(dataScroll, GUILayout.ExpandHeight(true));
|
||||
|
||||
foreach(var field in payloadFields) {
|
||||
DrawPayloadField(field, 0);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawPayloadField(PayloadField field, int depth) {
|
||||
if(field.children != null && field.children.Count > 0) {
|
||||
EditorGUI.indentLevel = depth;
|
||||
field.foldout = EditorGUILayout.Foldout(field.foldout, field.key, true, EditorStyles.foldoutHeader);
|
||||
if(field.foldout) {
|
||||
foreach(var child in field.children) {
|
||||
DrawPayloadField(child, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
EditorGUI.indentLevel = depth;
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField(field.key, labelStyle, GUILayout.Width(180));
|
||||
EditorGUILayout.LabelField(field.value, valueStyle);
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
loadedEnvelope = null;
|
||||
payloadFields.Clear();
|
||||
|
||||
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<SlotEntry>>();
|
||||
foreach(var slot in index.slots) {
|
||||
if(!slotsBySession.ContainsKey(slot.sessionId)) {
|
||||
slotsBySession[slot.sessionId] = new List<SlotEntry>();
|
||||
}
|
||||
slotsBySession[slot.sessionId].Add(new SlotEntry {
|
||||
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($"[SaveDataViewer] Failed to read save index: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSlot(SlotEntry slot) {
|
||||
selectedSlot = slot;
|
||||
loadedEnvelope = null;
|
||||
payloadFields.Clear();
|
||||
|
||||
var fullPath = Path.Combine(savePath, slot.filePath);
|
||||
if(!File.Exists(fullPath)) {
|
||||
Debug.LogWarning($"[SaveDataViewer] Save file not found: {fullPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
var bytes = File.ReadAllBytes(fullPath);
|
||||
var serializer = new JsonSaveSerializer();
|
||||
loadedEnvelope = serializer.Deserialize<SaveEnvelope>(bytes);
|
||||
|
||||
if(loadedEnvelope?.payload == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
payloadFields = ParseToken(loadedEnvelope.payload);
|
||||
}
|
||||
catch(Exception e) {
|
||||
Debug.LogError($"[SaveDataViewer] Failed to load save: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PayloadField> ParseToken(JToken token) {
|
||||
var fields = new List<PayloadField>();
|
||||
|
||||
if(token is JObject obj) {
|
||||
foreach(var property in obj.Properties()) {
|
||||
fields.Add(ParseProperty(property.Name, property.Value));
|
||||
}
|
||||
}
|
||||
else if(token is JArray arr) {
|
||||
for(var i = 0; i < arr.Count; i++) {
|
||||
fields.Add(ParseProperty($"[{i}]", arr[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static PayloadField ParseProperty(string key, JToken value) {
|
||||
switch(value.Type) {
|
||||
case JTokenType.Object: {
|
||||
var children = ParseToken(value);
|
||||
return new PayloadField { key = key, value = $"{{{children.Count} fields}}", children = children, foldout = false };
|
||||
}
|
||||
case JTokenType.Array: {
|
||||
var arr = (JArray)value;
|
||||
var children = new List<PayloadField>();
|
||||
for(var i = 0; i < arr.Count; i++) {
|
||||
children.Add(ParseProperty($"[{i}]", arr[i]));
|
||||
}
|
||||
return new PayloadField { key = $"{key} ({arr.Count})", value = $"[{arr.Count} items]", children = children, foldout = false };
|
||||
}
|
||||
case JTokenType.Null:
|
||||
return new PayloadField { key = key, value = "null" };
|
||||
default:
|
||||
return new PayloadField { key = key, value = value.ToString() };
|
||||
}
|
||||
}
|
||||
|
||||
// Internal types
|
||||
|
||||
[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<SlotEntry> slots;
|
||||
}
|
||||
|
||||
private sealed class SlotEntry {
|
||||
public string sessionId;
|
||||
public string displayName;
|
||||
public string filePath;
|
||||
public long timestampUtc;
|
||||
}
|
||||
|
||||
private sealed class PayloadField {
|
||||
public string key;
|
||||
public string value;
|
||||
public List<PayloadField> children;
|
||||
public bool foldout;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 08949eb1c0741e34b89fe2be65c1325b
|
||||
Reference in New Issue
Block a user