Lifecycle System Refactor & Logging Centralization (#56)

## ManagedBehaviour System Refactor

- **Sealed `Awake()`** to prevent override mistakes that break singleton registration
- **Added `OnManagedAwake()`** for early initialization (fires during registration)
- **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start)
- **40 files migrated** to new pattern (2 core, 38 components)
- Eliminated all fragile `private new void Awake()` patterns
- Zero breaking changes - backward compatible

## Centralized Logging System

- **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message`
- **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations
- **~90 logging call sites** migrated across 10 files
- **10 redundant helper methods** removed
- All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring

## Custom Log Console (Editor Window)

- **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection
- **Search** across class names, methods, and message content
- **Time range filter** with MinMaxSlider
- **Export** filtered logs to timestamped `.txt` files
- **Right-click context menu** for quick filtering and copy actions
- **Visual improvements:** White text, alternating row backgrounds, color-coded log levels
- **Multiple instances** supported for simultaneous system monitoring
- Open via `AppleHills > Custom Log Console`

Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com>
Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Reviewed-on: #56
This commit is contained in:
2025-11-11 08:48:29 +00:00
parent c4d356886f
commit 0aa2270e1a
68 changed files with 1541 additions and 1916 deletions

View File

@@ -71,7 +71,6 @@ namespace AppleHills.Editor
{
CardSystemManager.Instance.OnBoosterOpened += OnBoosterOpened;
CardSystemManager.Instance.OnCardCollected += OnCardCollected;
CardSystemManager.Instance.OnCardRarityUpgraded += OnCardRarityUpgraded;
CardSystemManager.Instance.OnBoosterCountChanged += OnBoosterCountChanged;
isSubscribed = true;
@@ -87,7 +86,6 @@ namespace AppleHills.Editor
{
CardSystemManager.Instance.OnBoosterOpened -= OnBoosterOpened;
CardSystemManager.Instance.OnCardCollected -= OnCardCollected;
CardSystemManager.Instance.OnCardRarityUpgraded -= OnCardRarityUpgraded;
CardSystemManager.Instance.OnBoosterCountChanged -= OnBoosterCountChanged;
}
@@ -109,13 +107,6 @@ namespace AppleHills.Editor
Repaint();
}
private void OnCardRarityUpgraded(CardData card)
{
lastEventMessage = $"Card upgraded: {card.Name} → {card.Rarity}!";
RefreshData();
Repaint();
}
private void OnBoosterCountChanged(int count)
{
boosterCount = count;

View File

@@ -1,4 +1,4 @@
using UnityEngine;
using UnityEngine;
using UnityEditor;
namespace Interactions

View File

@@ -0,0 +1,829 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Core;
namespace Editor
{
/// <summary>
/// Custom dockable log window with advanced filtering capabilities.
/// Supports filtering by class, method, log level, time range, and text search.
/// Multiple instances can be opened with independent filter states.
///
/// PERFORMANCE NOTE: For large log counts (10,000+), consider implementing virtualized scrolling.
/// Only render visible entries within the scroll view to avoid GUI overhead.
/// See DrawLogEntries() method for potential optimization location.
/// </summary>
public class CustomLogWindow : EditorWindow
{
#region Window State
private Vector2 scrollPosition;
private readonly List<LogEntry> allLogs = new List<LogEntry>();
#endregion
#region Filter State
private HashSet<string> activeClassTags = new HashSet<string>();
private HashSet<string> activeMethodTags = new HashSet<string>();
private HashSet<string> selectedClassFilters = new HashSet<string>();
private HashSet<string> selectedMethodFilters = new HashSet<string>();
private HashSet<LogLevel> selectedLevelFilters = new HashSet<LogLevel>
{
LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error
};
private string searchText = "";
private bool autoScroll = true;
// Time range filtering
private bool enableTimeFilter = false;
private float minTimestamp = 0;
private float maxTimestamp = 0;
private float currentMaxTimestamp = 0;
#endregion
#region UI State
// Colors for log levels
private readonly Color debugColor = Color.white;
private readonly Color infoColor = Color.white;
private readonly Color warningColor = Color.yellow;
private readonly Color errorColor = new Color(1f, 0.3f, 0.3f);
// Alternating row background colors
private readonly Color evenRowColor = new Color(0.22f, 0.22f, 0.22f);
private readonly Color oddRowColor = new Color(0.26f, 0.26f, 0.26f);
#endregion
#region Menu Items
[MenuItem("AppleHills/Custom Log Console")]
public static void ShowWindow()
{
var window = GetWindow<CustomLogWindow>("Custom Log");
window.Show();
}
#endregion
#region Unity Lifecycle
private void OnEnable()
{
// Subscribe to new log events
Logging.OnLogEntryAdded += OnLogAdded;
// Load existing logs
allLogs.AddRange(Logging.GetRecentLogs());
// Build initial tag lists
RebuildTagLists();
// Initialize time range
UpdateTimeRange();
}
private void OnDisable()
{
Logging.OnLogEntryAdded -= OnLogAdded;
}
private void OnLogAdded(LogEntry entry)
{
allLogs.Add(entry);
// Update active tags
activeClassTags.Add(entry.ClassName);
activeMethodTags.Add(entry.MethodName);
// Update time range
UpdateTimeRange();
if (autoScroll)
scrollPosition.y = float.MaxValue;
Repaint(); // Redraw window
}
#endregion
#region GUI Drawing
private void OnGUI()
{
DrawToolbar();
DrawLogEntries();
}
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
// Clear button
if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
allLogs.Clear();
activeClassTags.Clear();
activeMethodTags.Clear();
Logging.ClearLogs();
Repaint();
}
// Auto-scroll toggle
autoScroll = GUILayout.Toggle(autoScroll, "Auto-scroll", EditorStyles.toolbarButton, GUILayout.Width(80));
GUILayout.Space(10);
// Class Filter Button
string classLabel = selectedClassFilters.Count == 0 ? "Classes: All" :
selectedClassFilters.Count == activeClassTags.Count ? "Classes: All" :
$"Classes: {selectedClassFilters.Count}";
if (GUILayout.Button(new GUIContent(classLabel, "Filter by class"), EditorStyles.toolbarDropDown, GUILayout.Width(100)))
{
ShowClassFilterMenu();
}
// Method Filter Button
string methodLabel = selectedMethodFilters.Count == 0 ? "Methods: All" :
selectedMethodFilters.Count == activeMethodTags.Count ? "Methods: All" :
$"Methods: {selectedMethodFilters.Count}";
if (GUILayout.Button(new GUIContent(methodLabel, "Filter by method"), EditorStyles.toolbarDropDown, GUILayout.Width(110)))
{
ShowMethodFilterMenu();
}
// Log Level Filter Button
string levelLabel = selectedLevelFilters.Count == 4 ? "Levels: All" :
selectedLevelFilters.Count == 0 ? "Levels: None" :
$"Levels: {selectedLevelFilters.Count}";
if (GUILayout.Button(new GUIContent(levelLabel, "Filter by log level"), EditorStyles.toolbarDropDown, GUILayout.Width(90)))
{
ShowLevelFilterMenu();
}
// Time Range Filter Button
if (GUILayout.Button(new GUIContent("⏱", "Time range filter"), EditorStyles.toolbarButton, GUILayout.Width(25)))
{
ShowTimeRangeWindow();
}
GUILayout.FlexibleSpace();
// Search box
GUILayout.Label("Search:", GUILayout.Width(50));
searchText = GUILayout.TextField(searchText, EditorStyles.toolbarSearchField, GUILayout.Width(150));
GUILayout.Space(10);
// Export button
if (GUILayout.Button("Export", EditorStyles.toolbarButton, GUILayout.Width(60)))
{
ExportLogs();
}
GUILayout.Space(5);
// Log count
var filteredCount = GetFilteredLogs().Count();
GUILayout.Label($"{filteredCount}/{allLogs.Count}", GUILayout.Width(80));
EditorGUILayout.EndHorizontal();
}
private void ShowClassFilterMenu()
{
ClassFilterWindow.ShowWindow(this, activeClassTags, selectedClassFilters,
(newSelection) =>
{
selectedClassFilters = newSelection;
Repaint();
});
}
private void ShowMethodFilterMenu()
{
MethodFilterWindow.ShowWindow(this, activeMethodTags, selectedMethodFilters,
(newSelection) =>
{
selectedMethodFilters = newSelection;
Repaint();
});
}
private void ShowLevelFilterMenu()
{
LevelFilterWindow.ShowWindow(this, selectedLevelFilters,
(newSelection) =>
{
selectedLevelFilters = newSelection;
Repaint();
});
}
private void ShowTimeRangeWindow()
{
TimeRangeFilterWindow.ShowWindow(this, enableTimeFilter, minTimestamp, maxTimestamp, currentMaxTimestamp,
(enabled, min, max) =>
{
enableTimeFilter = enabled;
minTimestamp = min;
maxTimestamp = max;
Repaint();
});
}
private void DrawLogEntries()
{
// PERFORMANCE NOTE: For 10,000+ logs, consider virtualized scrolling here.
// Only render entries visible within the scroll view rect.
// Current implementation renders all filtered logs which may cause GUI slowdown.
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
var filteredLogs = GetFilteredLogs().ToList();
for (int i = 0; i < filteredLogs.Count; i++)
{
DrawLogEntry(filteredLogs[i], i);
}
EditorGUILayout.EndScrollView();
}
private void DrawLogEntry(LogEntry entry, int index)
{
// Draw alternating row background
Color bgColor = (index % 2 == 0) ? evenRowColor : oddRowColor;
Rect rowRect = EditorGUILayout.BeginHorizontal();
EditorGUI.DrawRect(rowRect, bgColor);
// Color-code by log level
Color color = GetColorForLevel(entry.Level);
var originalColor = GUI.contentColor;
GUI.contentColor = color;
// Timestamp
GUILayout.Label($"[{entry.Timestamp:F2}s]", GUILayout.Width(80));
// Level
GUILayout.Label($"[{entry.Level}]", GUILayout.Width(70));
// Class
GUILayout.Label($"[{entry.ClassName}]", GUILayout.Width(150));
// Method
GUILayout.Label($"[{entry.MethodName}]", GUILayout.Width(150));
// Message
GUILayout.Label(entry.Message, GUILayout.ExpandWidth(true));
EditorGUILayout.EndHorizontal();
GUI.contentColor = originalColor;
// Right-click context menu
if (Event.current.type == EventType.ContextClick && rowRect.Contains(Event.current.mousePosition))
{
GenericMenu menu = new GenericMenu();
menu.AddItem(new GUIContent("Copy Full Message"), false, () =>
{
EditorGUIUtility.systemCopyBuffer = entry.FullFormattedMessage;
});
menu.AddItem(new GUIContent("Copy Message Only"), false, () =>
{
EditorGUIUtility.systemCopyBuffer = entry.Message;
});
menu.AddSeparator("");
menu.AddItem(new GUIContent("Filter to this Class"), false, () =>
{
selectedClassFilters.Clear();
selectedClassFilters.Add(entry.ClassName);
Repaint();
});
menu.AddItem(new GUIContent("Filter to this Method"), false, () =>
{
selectedMethodFilters.Clear();
selectedMethodFilters.Add(entry.MethodName);
Repaint();
});
menu.AddItem(new GUIContent("Filter to this Class + Method"), false, () =>
{
selectedClassFilters.Clear();
selectedClassFilters.Add(entry.ClassName);
selectedMethodFilters.Clear();
selectedMethodFilters.Add(entry.MethodName);
Repaint();
});
menu.AddSeparator("");
menu.AddItem(new GUIContent("Filter to this Log Level"), false, () =>
{
selectedLevelFilters.Clear();
selectedLevelFilters.Add(entry.Level);
Repaint();
});
menu.ShowAsContext();
Event.current.Use();
}
}
#endregion
#region Filtering Logic
private IEnumerable<LogEntry> GetFilteredLogs()
{
return allLogs.Where(entry =>
{
// Filter by class (if any selected)
if (selectedClassFilters.Count > 0 && !selectedClassFilters.Contains(entry.ClassName))
return false;
// Filter by method (if any selected)
if (selectedMethodFilters.Count > 0 && !selectedMethodFilters.Contains(entry.MethodName))
return false;
// Filter by log level
if (!selectedLevelFilters.Contains(entry.Level))
return false;
// Filter by time range
if (enableTimeFilter)
{
if (entry.Timestamp < minTimestamp || entry.Timestamp > maxTimestamp)
return false;
}
// Filter by search text
if (!string.IsNullOrEmpty(searchText))
{
bool matchesSearch =
entry.ClassName.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
entry.MethodName.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
entry.Message.Contains(searchText, StringComparison.OrdinalIgnoreCase);
if (!matchesSearch)
return false;
}
return true;
});
}
#endregion
#region Helper Methods
private Color GetColorForLevel(LogLevel level)
{
return level switch
{
LogLevel.Debug => debugColor,
LogLevel.Info => infoColor,
LogLevel.Warning => warningColor,
LogLevel.Error => errorColor,
_ => infoColor
};
}
private void RebuildTagLists()
{
activeClassTags.Clear();
activeMethodTags.Clear();
foreach (var log in allLogs)
{
activeClassTags.Add(log.ClassName);
activeMethodTags.Add(log.MethodName);
}
}
private void UpdateTimeRange()
{
if (allLogs.Count > 0)
{
currentMaxTimestamp = allLogs.Max(l => l.Timestamp);
// Initialize time range on first log
if (maxTimestamp == 0)
{
maxTimestamp = currentMaxTimestamp;
}
else
{
// Auto-expand max range as new logs come in
maxTimestamp = currentMaxTimestamp;
}
}
}
private void ExportLogs()
{
string defaultFileName = $"logs_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt";
string path = EditorUtility.SaveFilePanel("Export Logs", "", defaultFileName, "txt");
if (!string.IsNullOrEmpty(path))
{
try
{
var filteredLogs = GetFilteredLogs().ToList();
var lines = filteredLogs.Select(e => e.FullFormattedMessage);
File.WriteAllLines(path, lines);
EditorUtility.DisplayDialog("Export Successful",
$"Exported {filteredLogs.Count} log entries to:\n{path}", "OK");
}
catch (Exception ex)
{
EditorUtility.DisplayDialog("Export Failed",
$"Failed to export logs:\n{ex.Message}", "OK");
}
}
}
#endregion
}
/// <summary>
/// Persistent popup window for class filtering with multi-selection support
/// </summary>
public class ClassFilterWindow : EditorWindow
{
private HashSet<string> availableTags;
private HashSet<string> selectedTags;
private System.Action<HashSet<string>> onApply;
private Vector2 scrollPosition;
private string searchFilter = "";
public static void ShowWindow(CustomLogWindow parent, HashSet<string> available, HashSet<string> selected,
System.Action<HashSet<string>> applyCallback)
{
var window = CreateInstance<ClassFilterWindow>();
window.titleContent = new GUIContent("Class Filter");
window.availableTags = available;
window.selectedTags = new HashSet<string>(selected);
window.onApply = applyCallback;
var parentRect = parent.position;
window.position = new Rect(parentRect.x + 50, parentRect.y + 50, 300, 400);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.Space(5);
// Control buttons
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("All", GUILayout.Width(60)))
{
if (availableTags != null)
selectedTags = new HashSet<string>(availableTags);
}
if (GUILayout.Button("None", GUILayout.Width(60)))
{
selectedTags.Clear();
}
GUILayout.FlexibleSpace();
searchFilter = EditorGUILayout.TextField(searchFilter, EditorStyles.toolbarSearchField, GUILayout.Width(150));
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Scrollable list of toggles
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
if (availableTags != null && availableTags.Count > 0)
{
var filteredTags = string.IsNullOrEmpty(searchFilter)
? availableTags.OrderBy(t => t)
: availableTags.Where(t => t.Contains(searchFilter, StringComparison.OrdinalIgnoreCase)).OrderBy(t => t);
foreach (var tag in filteredTags)
{
bool isSelected = selectedTags.Contains(tag);
bool newSelection = EditorGUILayout.ToggleLeft(tag, isSelected);
if (newSelection != isSelected)
{
if (newSelection)
selectedTags.Add(tag);
else
selectedTags.Remove(tag);
}
}
}
else
{
EditorGUILayout.HelpBox("No classes available", MessageType.Info);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(5);
// Apply/Close buttons
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Apply", GUILayout.Width(80)))
{
onApply?.Invoke(selectedTags);
Close();
}
if (GUILayout.Button("Close", GUILayout.Width(80)))
{
Close();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
}
}
/// <summary>
/// Persistent popup window for method filtering with multi-selection support
/// </summary>
public class MethodFilterWindow : EditorWindow
{
private HashSet<string> availableTags;
private HashSet<string> selectedTags;
private System.Action<HashSet<string>> onApply;
private Vector2 scrollPosition;
private string searchFilter = "";
public static void ShowWindow(CustomLogWindow parent, HashSet<string> available, HashSet<string> selected,
System.Action<HashSet<string>> applyCallback)
{
var window = CreateInstance<MethodFilterWindow>();
window.titleContent = new GUIContent("Method Filter");
window.availableTags = available;
window.selectedTags = new HashSet<string>(selected);
window.onApply = applyCallback;
var parentRect = parent.position;
window.position = new Rect(parentRect.x + 50, parentRect.y + 50, 300, 400);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.Space(5);
// Control buttons
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("All", GUILayout.Width(60)))
{
if (availableTags != null)
selectedTags = new HashSet<string>(availableTags);
}
if (GUILayout.Button("None", GUILayout.Width(60)))
{
selectedTags.Clear();
}
GUILayout.FlexibleSpace();
searchFilter = EditorGUILayout.TextField(searchFilter, EditorStyles.toolbarSearchField, GUILayout.Width(150));
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
// Scrollable list of toggles
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
if (availableTags != null && availableTags.Count > 0)
{
var filteredTags = string.IsNullOrEmpty(searchFilter)
? availableTags.OrderBy(t => t)
: availableTags.Where(t => t.Contains(searchFilter, StringComparison.OrdinalIgnoreCase)).OrderBy(t => t);
foreach (var tag in filteredTags)
{
bool isSelected = selectedTags.Contains(tag);
bool newSelection = EditorGUILayout.ToggleLeft(tag, isSelected);
if (newSelection != isSelected)
{
if (newSelection)
selectedTags.Add(tag);
else
selectedTags.Remove(tag);
}
}
}
else
{
EditorGUILayout.HelpBox("No methods available", MessageType.Info);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space(5);
// Apply/Close buttons
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Apply", GUILayout.Width(80)))
{
onApply?.Invoke(selectedTags);
Close();
}
if (GUILayout.Button("Close", GUILayout.Width(80)))
{
Close();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
}
}
/// <summary>
/// Persistent popup window for log level filtering with multi-selection support
/// </summary>
public class LevelFilterWindow : EditorWindow
{
private HashSet<LogLevel> selectedLevels;
private System.Action<HashSet<LogLevel>> onApply;
public static void ShowWindow(CustomLogWindow parent, HashSet<LogLevel> selected,
System.Action<HashSet<LogLevel>> applyCallback)
{
var window = CreateInstance<LevelFilterWindow>();
window.titleContent = new GUIContent("Log Level Filter");
window.selectedLevels = new HashSet<LogLevel>(selected);
window.onApply = applyCallback;
var parentRect = parent.position;
window.position = new Rect(parentRect.x + 50, parentRect.y + 50, 250, 180);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.Space(5);
// Control buttons
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("All", GUILayout.Width(60)))
{
selectedLevels = new HashSet<LogLevel>
{
LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error
};
}
if (GUILayout.Button("None", GUILayout.Width(60)))
{
selectedLevels.Clear();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
// Log level toggles
foreach (LogLevel level in Enum.GetValues(typeof(LogLevel)))
{
bool isSelected = selectedLevels.Contains(level);
bool newSelection = EditorGUILayout.ToggleLeft(level.ToString(), isSelected);
if (newSelection != isSelected)
{
if (newSelection)
selectedLevels.Add(level);
else
selectedLevels.Remove(level);
}
}
EditorGUILayout.Space(10);
// Apply/Close buttons
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Apply", GUILayout.Width(80)))
{
onApply?.Invoke(selectedLevels);
Close();
}
if (GUILayout.Button("Close", GUILayout.Width(80)))
{
Close();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(5);
}
}
/// <summary>
/// Popup window for configuring time range filters
/// </summary>
public class TimeRangeFilterWindow : EditorWindow
{
private bool enableTimeFilter;
private float minTimestamp;
private float maxTimestamp;
private float currentMaxTimestamp;
private System.Action<bool, float, float> onApply;
public static void ShowWindow(CustomLogWindow parent, bool enabled, float min, float max, float currentMax,
System.Action<bool, float, float> applyCallback)
{
var window = CreateInstance<TimeRangeFilterWindow>();
window.titleContent = new GUIContent("Time Range Filter");
window.enableTimeFilter = enabled;
window.minTimestamp = min;
window.maxTimestamp = max;
window.currentMaxTimestamp = currentMax;
window.onApply = applyCallback;
// Position near the parent window
var parentRect = parent.position;
window.position = new Rect(parentRect.x + 100, parentRect.y + 100, 350, 120);
window.ShowUtility();
}
private void OnGUI()
{
EditorGUILayout.Space(10);
enableTimeFilter = EditorGUILayout.Toggle("Enable Time Filter", enableTimeFilter);
EditorGUILayout.Space(5);
if (enableTimeFilter && currentMaxTimestamp > 0)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField($"Min: {minTimestamp:F2}s", GUILayout.Width(100));
EditorGUILayout.LabelField($"Max: {maxTimestamp:F2}s", GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
EditorGUILayout.MinMaxSlider(
ref minTimestamp,
ref maxTimestamp,
0,
currentMaxTimestamp);
EditorGUILayout.Space(5);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Reset Range"))
{
minTimestamp = 0;
maxTimestamp = currentMaxTimestamp;
}
EditorGUILayout.EndHorizontal();
}
else if (enableTimeFilter)
{
EditorGUILayout.HelpBox("No logs available for time filtering", MessageType.Info);
}
EditorGUILayout.Space(10);
EditorGUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Apply", GUILayout.Width(80)))
{
onApply?.Invoke(enableTimeFilter, minTimestamp, maxTimestamp);
Close();
}
if (GUILayout.Button("Cancel", GUILayout.Width(80)))
{
Close();
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(10);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 60a300585fef4ac990c963c8b37c3887
timeCreated: 1762814971

View File

@@ -181,7 +181,7 @@ namespace Editor
string scenePath = AssetDatabase.GUIDToAssetPath(guid);
var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive);
var allComponents = GameObject.FindObjectsOfType<Component>(true);
var allComponents = GameObject.FindObjectsByType<Component>(FindObjectsInactive.Include, FindObjectsSortMode.None);
foreach (var component in allComponents)
{
if (component == null || component.gameObject.scene != scene) continue;

View File

@@ -12,7 +12,7 @@ namespace Editor.Lifecycle
/// Editor-only bootstrap that ensures OnSceneReady is triggered when playing directly from a scene in Unity Editor.
///
/// PROBLEM: When you press Play in the editor without going through the scene manager:
/// - CustomBoot runs and triggers OnBootCompletionTriggered (which broadcasts OnManagedAwake)
/// - CustomBoot runs and triggers OnBootCompletionTriggered (which broadcasts OnManagedStart)
/// - But BroadcastSceneReady is NEVER called for the initial scene
/// - Components in the scene never receive their OnSceneReady() callback
///

View File

@@ -1,8 +1,6 @@
using UnityEditor;
using AppleHills.Core.Settings;
using Core;
using UnityEngine;
using UnityEngine.Rendering.VirtualTexturing;
namespace AppleHills.Editor
{
@@ -65,7 +63,7 @@ namespace AppleHills.Editor
GetPuzzlePromptRange
);
LogDebugMessage("Editor settings loaded for Scene View use");
Logging.Debug("Editor settings loaded for Scene View use");
}
public static void RefreshSceneViews()
@@ -102,14 +100,5 @@ namespace AppleHills.Editor
return null;
}
private static void LogDebugMessage(string message)
{
if (Application.isPlaying &&
DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().settingsLogVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[EditorSettingsProvider] {message}");
}
}
}
}

View File

@@ -1,153 +0,0 @@
using Interactions;
using PuzzleS;
using UnityEditor;
using UnityEngine;
namespace Editor
{
public class ItemPrefabEditorWindow : EditorWindow
{
private GameObject _selectedGameObject;
private InteractableBase _interactable;
private PickupItemData _pickupData;
private PuzzleStepSO _objectiveData;
private UnityEditor.Editor _soEditor;
private string _pickupSoFolderPath = "Assets/Data/Items";
private string _puzzleSoFolderPath = "Assets/Data/Puzzles";
private enum ItemType { None, Pickup, ItemSlot }
private ItemType _itemType = ItemType.None;
[MenuItem("AppleHills/Item Prefab Editor")]
public static void ShowWindow()
{
var window = GetWindow<ItemPrefabEditorWindow>("Item Prefab Editor");
window.minSize = new Vector2(400, 400);
}
private void OnEnable()
{
Selection.selectionChanged += Repaint;
}
private void OnDisable()
{
Selection.selectionChanged -= Repaint;
}
private void OnGUI()
{
_selectedGameObject = null;
_interactable = null;
if (Selection.activeGameObject != null)
{
_selectedGameObject = Selection.activeGameObject;
_interactable = _selectedGameObject.GetComponent<InteractableBase>();
}
else if (Selection.activeObject is GameObject go)
{
_selectedGameObject = go;
_interactable = go.GetComponent<InteractableBase>();
}
if (_selectedGameObject == null || _interactable == null)
{
EditorGUILayout.HelpBox("Select a GameObject or prefab with an InteractableBase component to edit.", MessageType.Info);
return;
}
EditorGUILayout.LabelField("Editing: ", _selectedGameObject.name, EditorStyles.boldLabel);
EditorGUILayout.Space();
// Determine current type
bool hasPickup = _selectedGameObject.GetComponent<Pickup>() != null;
bool hasSlot = _selectedGameObject.GetComponent<ItemSlot>() != null;
if (hasSlot) _itemType = ItemType.ItemSlot;
else if (hasPickup) _itemType = ItemType.Pickup;
else _itemType = ItemType.None;
// Item type selection
var newType = (ItemType)EditorGUILayout.EnumPopup("Item Type", _itemType);
if (newType != _itemType)
{
// Remove both, then add selected
PrefabEditorUtility.RemoveComponent<Pickup>(_selectedGameObject);
PrefabEditorUtility.RemoveComponent<ItemSlot>(_selectedGameObject);
if (newType == ItemType.Pickup)
PrefabEditorUtility.AddOrGetComponent<Pickup>(_selectedGameObject);
else if (newType == ItemType.ItemSlot)
PrefabEditorUtility.AddOrGetComponent<ItemSlot>(_selectedGameObject);
_itemType = newType;
}
// ObjectiveStepBehaviour
bool hasObjective = _selectedGameObject.GetComponent<ObjectiveStepBehaviour>() != null;
bool addObjective = EditorGUILayout.Toggle("ObjectiveStepBehaviour", hasObjective);
if (addObjective && !hasObjective)
{
PrefabEditorUtility.AddOrGetComponent<ObjectiveStepBehaviour>(_selectedGameObject);
}
else if (!addObjective && hasObjective)
{
PrefabEditorUtility.RemoveComponent<ObjectiveStepBehaviour>(_selectedGameObject);
}
// Pickup Data (for Pickup or ItemSlot)
if (_itemType == ItemType.Pickup || _itemType == ItemType.ItemSlot)
{
var pickup = _selectedGameObject.GetComponent<Pickup>();
_pickupData = pickup.itemData;
EditorGUILayout.LabelField("Pickup Data:", EditorStyles.boldLabel);
_pickupData = (PickupItemData)EditorGUILayout.ObjectField("PickupItemData", _pickupData, typeof(PickupItemData), false);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_pickupSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_pickupSoFolderPath = PrefabEditorUtility.SelectFolder(_pickupSoFolderPath, "Data/Items");
}
EditorGUILayout.EndHorizontal();
if (_pickupData == null && GUILayout.Button("Create New PickupItemData"))
{
_pickupData = PrefabEditorUtility.CreateScriptableAsset<PickupItemData>(_selectedGameObject.name + "_pickup", _pickupSoFolderPath);
}
if (_pickupData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _pickupData);
pickup.itemData = _pickupData;
}
}
// Objective Data
if (addObjective)
{
var obj = _selectedGameObject.GetComponent<ObjectiveStepBehaviour>();
_objectiveData = obj.stepData;
EditorGUILayout.LabelField("Objective Data:", EditorStyles.boldLabel);
_objectiveData = (PuzzleStepSO)EditorGUILayout.ObjectField("PuzzleStepSO", _objectiveData, typeof(PuzzleStepSO), false);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_puzzleSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_puzzleSoFolderPath = PrefabEditorUtility.SelectFolder(_puzzleSoFolderPath, "Data/Puzzles");
}
EditorGUILayout.EndHorizontal();
if (_objectiveData == null && GUILayout.Button("Create New PuzzleStepSO"))
{
_objectiveData = PrefabEditorUtility.CreateScriptableAsset<PuzzleStepSO>(_selectedGameObject.name + "_puzzle", _puzzleSoFolderPath);
}
if (_objectiveData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _objectiveData);
obj.stepData = _objectiveData;
}
}
if (GUI.changed)
{
EditorUtility.SetDirty(_selectedGameObject);
PrefabUtility.RecordPrefabInstancePropertyModifications(_selectedGameObject);
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: 943b203cde5343c68a6278c111fce2ed
timeCreated: 1757508162

View File

@@ -1,168 +0,0 @@
using UnityEditor;
using UnityEngine;
using System.IO;
using Interactions;
using PuzzleS;
namespace Editor
{
public class PrefabCreatorWindow : EditorWindow
{
private string _prefabName = "NewPrefab";
private string _saveFolderPath = "Assets/Prefabs/Items";
private string _pickupSoFolderPath = "Assets/Data/Items";
private string _puzzleSoFolderPath = "Assets/Data/Puzzles";
private bool _addObjective;
private PickupItemData _pickupData;
private PuzzleStepSO _objectiveData;
private UnityEditor.Editor _soEditor;
private enum ItemType { None, Pickup, ItemSlot }
private ItemType _itemType = ItemType.None;
private bool _createNext = false;
[MenuItem("AppleHills/Item Prefab Creator")]
public static void ShowWindow()
{
var window = GetWindow<PrefabCreatorWindow>("Prefab Creator");
window.minSize = new Vector2(400, 400);
// Set default paths if not already set
if (string.IsNullOrEmpty(window._saveFolderPath))
window._saveFolderPath = "Assets/Prefabs/Items";
if (string.IsNullOrEmpty(window._pickupSoFolderPath))
window._pickupSoFolderPath = "Assets/Data/Items";
if (string.IsNullOrEmpty(window._puzzleSoFolderPath))
window._puzzleSoFolderPath = "Assets/Data/Puzzles";
}
private void OnGUI()
{
EditorGUILayout.LabelField("Prefab Creator", EditorStyles.boldLabel);
_prefabName = EditorGUILayout.TextField("Prefab Name", _prefabName);
// Prefab save folder
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save Folder");
EditorGUILayout.SelectableLabel(_saveFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_saveFolderPath = PrefabEditorUtility.SelectFolder(_saveFolderPath, "Prefabs/Items");
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
EditorGUILayout.LabelField("Add Components:", EditorStyles.boldLabel);
// Item type selection
var newType = (ItemType)EditorGUILayout.EnumPopup("Item Type", _itemType);
if (newType != _itemType)
{
_itemType = newType;
}
bool addObjective = EditorGUILayout.Toggle("ObjectiveStepBehaviour", _addObjective);
_addObjective = addObjective;
EditorGUILayout.Space();
// Pickup Data (for Pickup or ItemSlot)
if (_itemType == ItemType.Pickup || _itemType == ItemType.ItemSlot)
{
EditorGUILayout.LabelField("Pickup Data:", EditorStyles.boldLabel);
_pickupData = (PickupItemData)EditorGUILayout.ObjectField("PickupItemData", _pickupData, typeof(PickupItemData), false);
// Pickup SO save folder
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_pickupSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_pickupSoFolderPath = PrefabEditorUtility.SelectFolder(_pickupSoFolderPath, "Data/Items");
}
EditorGUILayout.EndHorizontal();
if (_pickupData == null && GUILayout.Button("Create New PickupItemData"))
{
_pickupData = PrefabEditorUtility.CreateScriptableAsset<PickupItemData>(_prefabName + "_pickup", _pickupSoFolderPath);
}
if (_pickupData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _pickupData);
}
}
// Objective Data
if (_addObjective)
{
EditorGUILayout.LabelField("Objective Data:", EditorStyles.boldLabel);
_objectiveData = (PuzzleStepSO)EditorGUILayout.ObjectField("PuzzleStepSO", _objectiveData, typeof(PuzzleStepSO), false);
// Puzzle SO save folder
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Save To");
EditorGUILayout.SelectableLabel(_puzzleSoFolderPath, EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
if (GUILayout.Button("Select...", GUILayout.Width(80)))
{
_puzzleSoFolderPath = PrefabEditorUtility.SelectFolder(_puzzleSoFolderPath, "Data/Puzzles");
}
EditorGUILayout.EndHorizontal();
if (_objectiveData == null && GUILayout.Button("Create New PuzzleStepSO"))
{
_objectiveData = PrefabEditorUtility.CreateScriptableAsset<PuzzleStepSO>(_prefabName + "_puzzle", _puzzleSoFolderPath);
}
if (_objectiveData != null)
{
PrefabEditorUtility.DrawScriptableObjectEditor(ref _soEditor, _objectiveData);
}
}
GUILayout.FlexibleSpace();
EditorGUILayout.BeginHorizontal();
GUI.enabled = !string.IsNullOrEmpty(_prefabName) && !string.IsNullOrEmpty(_saveFolderPath);
if (GUILayout.Button("Create Prefab", GUILayout.Height(28)))
{
CreatePrefab();
}
_createNext = GUILayout.Toggle(_createNext, "Create Next", GUILayout.Width(100), GUILayout.Height(28));
GUI.enabled = true;
EditorGUILayout.EndHorizontal();
}
private void CreatePrefab()
{
var go = new GameObject(_prefabName);
// Note: No need to add InteractableBase separately - Pickup and ItemSlot inherit from it
go.AddComponent<BoxCollider>();
int interactableLayer = LayerMask.NameToLayer("Interactable");
if (interactableLayer != -1)
go.layer = interactableLayer;
go.AddComponent<SpriteRenderer>();
if (_itemType == ItemType.Pickup)
{
var pickup = go.AddComponent<Pickup>();
pickup.itemData = _pickupData;
}
else if (_itemType == ItemType.ItemSlot)
{
var slot = go.AddComponent<ItemSlot>();
slot.itemData = _pickupData;
}
if (_addObjective)
{
var obj = go.AddComponent<ObjectiveStepBehaviour>();
obj.stepData = _objectiveData;
}
string prefabPath = Path.Combine(_saveFolderPath, _prefabName + ".prefab").Replace("\\", "/");
var prefab = PrefabUtility.SaveAsPrefabAsset(go, prefabPath);
DestroyImmediate(go);
AssetDatabase.Refresh();
Selection.activeObject = prefab;
EditorGUIUtility.PingObject(prefab);
EditorUtility.DisplayDialog("Prefab Created", $"Prefab saved to {prefabPath}", "OK");
if (_createNext)
{
_prefabName = "NewPrefab";
_pickupData = null;
_objectiveData = null;
_itemType = ItemType.None;
_addObjective = false;
_soEditor = null;
GUI.FocusControl(null);
Repaint();
}
}
}
}

View File

@@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: f67e06e997f642509ba61ea12b0f793e
timeCreated: 1757503955

File diff suppressed because one or more lines are too long

View File

@@ -56,7 +56,7 @@ public class BirdEyesBehavior : ManagedBehaviour
_statemachine.ChangeState("BirdSpawned");
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
base.OnSceneRestoreRequested(serializedData);
@@ -75,7 +75,7 @@ public class BirdEyesBehavior : ManagedBehaviour
}
}
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
return _wolterisoutTriggered.ToString();
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using AppleHills.Core.Settings;
using UnityEngine;
using Core;
@@ -33,11 +33,9 @@ namespace Bootstrap
// Run very early - need to set up loading screen before other systems initialize
public override int ManagedAwakePriority => 5;
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // Register with LifecycleManager
LogDebugMessage("BootSceneController.Awake() - Initializing loading screen DURING bootstrap");
Logging.Debug("BootSceneController.Awake() - Initializing loading screen DURING bootstrap");
// Validate loading screen exists
if (initialLoadingScreen == null)
@@ -71,11 +69,11 @@ namespace Bootstrap
}
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
LogDebugMessage("BootSceneController.OnManagedAwake() - Boot is GUARANTEED complete, starting scene loading");
Logging.Debug("BootSceneController.OnManagedStart() - Boot is GUARANTEED complete, starting scene loading");
// Boot is GUARANTEED complete at this point - that's the whole point of OnManagedAwake!
// Boot is GUARANTEED complete at this point - that's the whole point of OnManagedStart!
// No need to subscribe to OnBootCompleted or check CustomBoot.Initialised
_bootComplete = true;
_currentPhase = LoadingPhase.SceneLoading;
@@ -102,12 +100,12 @@ namespace Bootstrap
/// </summary>
private void OnInitialLoadingComplete()
{
LogDebugMessage("Initial loading screen fully hidden, boot sequence completed");
Logging.Debug("Initial loading screen fully hidden, boot sequence completed");
// Play the intro cinematic if available
if (CinematicsManager.Instance != null)
{
LogDebugMessage("Attempting to play intro cinematic");
Logging.Debug("Attempting to play intro cinematic");
// Use LoadAndPlayCinematic to play the intro sequence
CinematicsManager.Instance.LoadAndPlayCinematic("IntroSequence", false);
@@ -149,13 +147,13 @@ namespace Bootstrap
{
if (debugMode)
{
LogDebugMessage($"Bootstrap progress: {progress:P0}, Combined: {GetCombinedProgress():P0}");
Logging.Debug($"Bootstrap progress: {progress:P0}, Combined: {GetCombinedProgress():P0}");
}
}
private void LogDebugInfo()
{
LogDebugMessage($"Debug - Phase: {_currentPhase}, Bootstrap: {CustomBoot.CurrentProgress:P0}, " +
Logging.Debug($"Debug - Phase: {_currentPhase}, Bootstrap: {CustomBoot.CurrentProgress:P0}, " +
$"Scene: {_sceneLoadingProgress:P0}, Combined: {GetCombinedProgress():P0}, Boot Complete: {_bootComplete}");
}
@@ -172,7 +170,7 @@ namespace Bootstrap
private async void LoadMainScene()
{
LogDebugMessage($"Loading main menu scene: {mainSceneName}");
Logging.Debug($"Loading main menu scene: {mainSceneName}");
try
{
@@ -186,7 +184,7 @@ namespace Bootstrap
if (debugMode)
{
LogDebugMessage($"Scene loading raw: {value:P0}, Combined: {GetCombinedProgress():P0}");
Logging.Debug($"Scene loading raw: {value:P0}, Combined: {GetCombinedProgress():P0}");
}
});
@@ -210,13 +208,13 @@ namespace Bootstrap
_sceneLoadingProgress = 1f;
// CRITICAL: Broadcast lifecycle events so components get their OnSceneReady callbacks
LogDebugMessage($"Broadcasting OnSceneReady for: {mainSceneName}");
Logging.Debug($"Broadcasting OnSceneReady for: {mainSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(mainSceneName);
// Restore scene data for the main menu
if (SaveLoadManager.Instance != null)
{
LogDebugMessage($"Restoring scene data for: {mainSceneName}");
Logging.Debug($"Restoring scene data for: {mainSceneName}");
SaveLoadManager.Instance.RestoreSceneData();
}
@@ -246,7 +244,7 @@ namespace Bootstrap
Scene currentScene = SceneManager.GetActiveScene();
string startingSceneName = currentScene.name;
LogDebugMessage($"Unloading StartingScene: {startingSceneName}");
Logging.Debug($"Unloading StartingScene: {startingSceneName}");
// Unload the StartingScene
await SceneManager.UnloadSceneAsync(startingSceneName);
@@ -255,14 +253,14 @@ namespace Bootstrap
Scene mainMenuScene = SceneManager.GetSceneByName(mainSceneName);
SceneManager.SetActiveScene(mainMenuScene);
LogDebugMessage($"Transition complete: {startingSceneName} unloaded, {mainSceneName} is now active");
Logging.Debug($"Transition complete: {startingSceneName} unloaded, {mainSceneName} is now active");
// Destroy the boot scene controller since its job is done
Destroy(gameObject);
}
catch (Exception e)
{
Logging.Warning($"[BootSceneController] Error unloading StartingScene: {e.Message}");
Logging.Warning($"Error unloading StartingScene: {e.Message}");
}
}
@@ -283,13 +281,5 @@ namespace Bootstrap
_progressAction?.Invoke(value);
}
}
private void LogDebugMessage(string message)
{
if ( _logVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[BootSceneController] {message}");
}
}
}
}

View File

@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using UnityEngine;
@@ -105,7 +104,7 @@ namespace Bootstrap
// Notify the LifecycleManager that boot is complete
if (Application.isPlaying)
{
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
Logging.Debug("Calling LifecycleManager.OnBootCompletionTriggered()");
if (LifecycleManager.Instance != null)
{
LifecycleManager.Instance.OnBootCompletionTriggered();
@@ -127,7 +126,7 @@ namespace Bootstrap
// Notify the LifecycleManager that boot is complete
if (Application.isPlaying)
{
LogDebugMessage("Calling LifecycleManager.OnBootCompletionTriggered()");
Logging.Debug("Calling LifecycleManager.OnBootCompletionTriggered()");
if (LifecycleManager.Instance != null)
{
LifecycleManager.Instance.OnBootCompletionTriggered();
@@ -238,16 +237,7 @@ namespace Bootstrap
{
CurrentProgress = Mathf.Clamp01(progress);
OnBootProgressChanged?.Invoke(CurrentProgress);
LogDebugMessage($"Progress: {CurrentProgress:P0}");
}
private static void LogDebugMessage(string message)
{
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().bootstrapLogVerbosity <=
LogVerbosity.Debug)
{
Logging.Debug($"[CustomBoot] {message}");
}
Logging.Debug($"Progress: {CurrentProgress:P0}");
}
}
}

View File

@@ -138,7 +138,7 @@ namespace Bootstrap
float displayProgress = Mathf.Min(steadyProgress, actualProgress);
// Log the progress values for debugging
LogDebugMessage($"Progress - Default: {steadyProgress:F2}, Actual: {actualProgress:F2}, Display: {displayProgress:F2}");
Logging.Debug($"Progress - Default: {steadyProgress:F2}, Actual: {actualProgress:F2}, Display: {displayProgress:F2}");
// Directly set the progress bar fill amount without smoothing
if (progressBarImage != null)
@@ -151,7 +151,7 @@ namespace Bootstrap
if (steadyProgress >= 1.0f && displayProgress >= 1.0f)
{
_animationComplete = true;
LogDebugMessage("Animation complete");
Logging.Debug("Animation complete");
break;
}
@@ -163,7 +163,7 @@ namespace Bootstrap
if (progressBarImage != null)
{
progressBarImage.fillAmount = 1.0f;
LogDebugMessage("Final progress set to 1.0");
Logging.Debug("Final progress set to 1.0");
}
// Hide the screen if loading is also complete
@@ -172,7 +172,7 @@ namespace Bootstrap
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(false);
LogDebugMessage("Animation AND loading complete, hiding screen");
Logging.Debug("Animation AND loading complete, hiding screen");
// Invoke the callback when fully hidden
_onLoadingScreenFullyHidden?.Invoke();
@@ -189,7 +189,7 @@ namespace Bootstrap
/// </summary>
public void HideLoadingScreen()
{
LogDebugMessage("Loading complete, marking loading as finished");
Logging.Debug("Loading complete, marking loading as finished");
// Mark that loading is complete
_loadingComplete = true;
@@ -200,7 +200,7 @@ namespace Bootstrap
if (loadingScreenContainer != null)
{
loadingScreenContainer.SetActive(false);
LogDebugMessage("Animation already complete, hiding screen immediately");
Logging.Debug("Animation already complete, hiding screen immediately");
// Invoke the callback when fully hidden
_onLoadingScreenFullyHidden?.Invoke();
@@ -210,7 +210,7 @@ namespace Bootstrap
}
else
{
LogDebugMessage("Animation still in progress, waiting for it to complete");
Logging.Debug("Animation still in progress, waiting for it to complete");
// The coroutine will handle hiding when animation completes
}
}
@@ -244,13 +244,5 @@ namespace Bootstrap
return tcs.Task;
}
private void LogDebugMessage(string message)
{
if ( _logVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[InitialLoadingScreen] {message}");
}
}
}
}

View File

@@ -39,15 +39,13 @@ namespace Cinematics
public override int ManagedAwakePriority => 170; // Cinematic systems
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
Logging.Debug("[CinematicsManager] Initialized");
}

View File

@@ -15,11 +15,10 @@ namespace Cinematics
private float _holdStartTime;
private bool _isHolding;
private bool _skipPerformed;
private bool _initialized = false;
public override int ManagedAwakePriority => 180; // Cinematic UI
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Reset the progress bar
if (radialProgressBar != null)

View File

@@ -37,14 +37,12 @@ namespace Core
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 10; // Core infrastructure - runs early
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Create settings providers - must happen in Awake so other managers can access settings in their ManagedAwake
// Create settings providers - must happen in OnManagedAwake so other managers can access settings in their ManagedStart
SettingsProvider.Instance.gameObject.name = "Settings Provider";
DeveloperSettingsProvider.Instance.gameObject.name = "Developer Settings Provider";
@@ -57,9 +55,9 @@ namespace Core
_managerLogVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().gameManagerLogVerbosity;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Settings are already initialized in Awake()
// Settings are already initialized in OnManagedAwake()
// This is available for future initialization that depends on other managers
}
@@ -79,7 +77,7 @@ namespace Core
component.Pause();
}
LogDebugMessage($"Registered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
Logging.Debug($"Registered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
}
}
@@ -92,7 +90,7 @@ namespace Core
if (component != null && _pausableComponents.Contains(component))
{
_pausableComponents.Remove(component);
LogDebugMessage($"Unregistered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
Logging.Debug($"Unregistered pausable component: {(component as MonoBehaviour)?.name ?? "Unknown"}");
}
}
@@ -108,7 +106,7 @@ namespace Core
ApplyPause(true);
}
LogDebugMessage($"Pause requested by {requester?.ToString() ?? "Unknown"}. pauseCount = {_pauseCount}");
Logging.Debug($"Pause requested by {requester?.ToString() ?? "Unknown"}. pauseCount = {_pauseCount}");
}
/// <summary>
@@ -123,7 +121,7 @@ namespace Core
ApplyPause(false);
}
LogDebugMessage($"Pause released by {requester?.ToString() ?? "Unknown"}. pauseCount = {_pauseCount}");
Logging.Debug($"Pause released by {requester?.ToString() ?? "Unknown"}. pauseCount = {_pauseCount}");
}
/// <summary>
@@ -162,12 +160,12 @@ namespace Core
OnGameResumed?.Invoke();
}
LogDebugMessage($"Game {(shouldPause ? "paused" : "resumed")}. Paused {_pausableComponents.Count} components.");
Logging.Debug($"Game {(shouldPause ? "paused" : "resumed")}. Paused {_pausableComponents.Count} components.");
}
private void InitializeSettings()
{
LogDebugMessage("Starting settings initialization...", "SettingsInitialization", _settingsLogVerbosity);
Logging.Debug("Starting settings initialization...");
// Load settings synchronously
var playerSettings = SettingsProvider.Instance.LoadSettingsSynchronous<PlayerFollowerSettings>();
@@ -178,7 +176,7 @@ namespace Core
if (playerSettings != null)
{
ServiceLocator.Register<IPlayerFollowerSettings>(playerSettings);
LogDebugMessage("PlayerFollowerSettings registered successfully", "SettingsInitialization", _settingsLogVerbosity);
Logging.Debug("PlayerFollowerSettings registered successfully");
}
else
{
@@ -188,7 +186,7 @@ namespace Core
if (interactionSettings != null)
{
ServiceLocator.Register<IInteractionSettings>(interactionSettings);
LogDebugMessage("InteractionSettings registered successfully", "SettingsInitialization", _settingsLogVerbosity);
Logging.Debug("InteractionSettings registered successfully");
}
else
{
@@ -198,7 +196,7 @@ namespace Core
if (minigameSettings != null)
{
ServiceLocator.Register<IDivingMinigameSettings>(minigameSettings);
LogDebugMessage("MinigameSettings registered successfully", "SettingsInitialization", _settingsLogVerbosity);
Logging.Debug("MinigameSettings registered successfully");
}
else
{
@@ -209,7 +207,7 @@ namespace Core
_settingsLoaded = playerSettings != null && interactionSettings != null && minigameSettings != null;
if (_settingsLoaded)
{
LogDebugMessage("All settings loaded and registered with ServiceLocator", "SettingsInitialization", _settingsLogVerbosity);
Logging.Debug("All settings loaded and registered with ServiceLocator");
}
else
{
@@ -222,7 +220,7 @@ namespace Core
/// </summary>
private void InitializeDeveloperSettings()
{
LogDebugMessage("Starting developer settings initialization...", "SettingsInitialization", _settingsLogVerbosity);
Logging.Debug("Starting developer settings initialization...");
// Load developer settings
var divingDevSettings = DeveloperSettingsProvider.Instance.GetSettings<DivingDeveloperSettings>();
@@ -232,7 +230,7 @@ namespace Core
if (_developerSettingsLoaded)
{
LogDebugMessage("All developer settings loaded successfully", "SettingsInitialization", _settingsLogVerbosity);
Logging.Debug("All developer settings loaded successfully");
}
else
{
@@ -266,19 +264,6 @@ namespace Core
{
return DeveloperSettingsProvider.Instance?.GetSettings<T>();
}
private void LogDebugMessage(string message, string prefix = "GameManager", LogVerbosity verbosity = LogVerbosity.None)
{
if (verbosity == LogVerbosity.None)
{
verbosity = _managerLogVerbosity;
}
if ( verbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[{prefix}] {message}");
}
}
// LEFTOVER LEGACY SETTINGS
public float PlayerStopDistance => GetSettings<IInteractionSettings>()?.PlayerStopDistance ?? 6.0f;

View File

@@ -50,20 +50,18 @@ namespace Core
public override int ManagedAwakePriority => 75; // Item registry
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
Logging.Debug("[ItemManager] Initialized");
}
protected override void OnSceneReady()
internal override void OnSceneReady()
{
// Replaces SceneLoadStarted subscription for clearing registrations
ClearAllRegistrations();
@@ -144,7 +142,6 @@ namespace Core
{
s.OnCorrectItemSlotted -= ItemSlot_OnCorrectItemSlotted;
s.OnIncorrectItemSlotted -= ItemSlot_OnIncorrectItemSlotted;
s.OnForbiddenItemSlotted -= ItemSlot_OnForbiddenItemSlotted;
s.OnItemSlotRemoved -= ItemSlot_OnItemSlotRemoved;
}
}
@@ -191,7 +188,6 @@ namespace Core
// Subscribe to all slot events
slot.OnCorrectItemSlotted += ItemSlot_OnCorrectItemSlotted;
slot.OnIncorrectItemSlotted += ItemSlot_OnIncorrectItemSlotted;
slot.OnForbiddenItemSlotted += ItemSlot_OnForbiddenItemSlotted;
slot.OnItemSlotRemoved += ItemSlot_OnItemSlotRemoved;
}
}
@@ -204,7 +200,6 @@ namespace Core
// Unsubscribe from all slot events
slot.OnCorrectItemSlotted -= ItemSlot_OnCorrectItemSlotted;
slot.OnIncorrectItemSlotted -= ItemSlot_OnIncorrectItemSlotted;
slot.OnForbiddenItemSlotted -= ItemSlot_OnForbiddenItemSlotted;
slot.OnItemSlotRemoved -= ItemSlot_OnItemSlotRemoved;
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using UnityEngine;
@@ -123,7 +123,24 @@ namespace Core.Lifecycle
// ALWAYS add to managedAwakeList - this is the master list used for save/load
InsertSorted(managedAwakeList, component, component.ManagedAwakePriority);
// Handle ManagedAwake timing based on boot state
// Register for all scene lifecycle hooks
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
InsertSorted(sceneReadyList, component, component.SceneReadyPriority);
InsertSorted(saveRequestedList, component, component.SavePriority);
InsertSorted(restoreRequestedList, component, component.RestorePriority);
InsertSorted(destroyList, component, component.DestroyPriority);
// Call OnManagedAwake immediately after registration (early initialization hook)
try
{
component.OnManagedAwake();
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
}
// Handle OnManagedStart timing based on boot state
if (isBootComplete)
{
// Check if we're currently loading a scene
@@ -136,27 +153,20 @@ namespace Core.Lifecycle
else
{
// Truly late registration (component enabled after scene is ready)
// Call OnManagedAwake immediately since boot already completed
LogDebug($"Late registration: Calling OnManagedAwake immediately for {component.gameObject.name}");
// Call OnManagedStart immediately since boot already completed
LogDebug($"Late registration: Calling OnManagedStart immediately for {component.gameObject.name}");
try
{
component.InvokeManagedAwake();
component.OnManagedStart();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
Debug.LogError($"[LifecycleManager] Error in OnManagedStart for {component.gameObject.name}: {ex}");
}
}
}
// If boot not complete, component stays in list and will be processed by BroadcastManagedAwake()
// Register for all scene lifecycle hooks
InsertSorted(sceneUnloadingList, component, component.SceneUnloadingPriority);
InsertSorted(sceneReadyList, component, component.SceneReadyPriority);
InsertSorted(saveRequestedList, component, component.SavePriority);
InsertSorted(restoreRequestedList, component, component.RestorePriority);
InsertSorted(destroyList, component, component.DestroyPriority);
// If boot not complete, component stays in list and will be processed by BroadcastManagedStart()
// If this scene is already ready (and we're not in loading mode), call OnSceneReady immediately
if (!isLoadingScene && currentSceneReady == sceneName)
@@ -164,7 +174,7 @@ namespace Core.Lifecycle
LogDebug($"Late registration: Calling OnSceneReady immediately for {component.gameObject.name}");
try
{
component.InvokeSceneReady();
component.OnSceneReady();
}
catch (Exception ex)
{
@@ -202,7 +212,7 @@ namespace Core.Lifecycle
/// <summary>
/// Called by CustomBoot when boot completes.
/// Broadcasts ManagedAwake to all registered components.
/// Broadcasts ManagedStart to all registered components.
/// </summary>
public void OnBootCompletionTriggered()
{
@@ -210,16 +220,16 @@ namespace Core.Lifecycle
return;
LogDebug("=== Boot Completion Triggered ===");
BroadcastManagedAwake();
BroadcastManagedStart();
isBootComplete = true;
}
/// <summary>
/// Broadcast OnManagedAwake to all registered components (priority ordered).
/// Broadcast OnManagedStart to all registered components (priority ordered).
/// </summary>
private void BroadcastManagedAwake()
private void BroadcastManagedStart()
{
LogDebug($"Broadcasting ManagedAwake to {managedAwakeList.Count} components");
LogDebug($"Broadcasting ManagedStart to {managedAwakeList.Count} components");
// Create a copy to avoid collection modification during iteration
var componentsCopy = new List<ManagedBehaviour>(managedAwakeList);
@@ -230,12 +240,12 @@ namespace Core.Lifecycle
try
{
component.InvokeManagedAwake();
component.OnManagedStart();
HandleAutoRegistrations(component);
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for {component.gameObject.name}: {ex}");
Debug.LogError($"[LifecycleManager] Error in OnManagedStart for {component.gameObject.name}: {ex}");
}
}
@@ -275,20 +285,20 @@ namespace Core.Lifecycle
// Sort by ManagedAwake priority (lower values first)
pendingSceneComponents.Sort((a, b) => a.ManagedAwakePriority.CompareTo(b.ManagedAwakePriority));
// Call OnManagedAwake in priority order
// Call OnManagedStart in priority order
foreach (var component in pendingSceneComponents)
{
if (component == null) continue;
try
{
component.InvokeManagedAwake();
component.OnManagedStart();
HandleAutoRegistrations(component);
LogDebug($"Processed batched component: {component.gameObject.name} (Priority: {component.ManagedAwakePriority})");
}
catch (Exception ex)
{
Debug.LogError($"[LifecycleManager] Error in OnManagedAwake for batched component {component.gameObject.name}: {ex}");
Debug.LogError($"[LifecycleManager] Error in OnManagedStart for batched component {component.gameObject.name}: {ex}");
}
}
@@ -315,7 +325,7 @@ namespace Core.Lifecycle
{
try
{
component.InvokeSceneUnloading();
component.OnSceneUnloading();
}
catch (Exception ex)
{
@@ -351,7 +361,7 @@ namespace Core.Lifecycle
{
try
{
component.InvokeSceneReady();
component.OnSceneReady();
}
catch (Exception ex)
{
@@ -379,7 +389,7 @@ namespace Core.Lifecycle
try
{
string serializedData = component.InvokeSceneSaveRequested();
string serializedData = component.OnSceneSaveRequested();
if (!string.IsNullOrEmpty(serializedData))
{
string saveId = component.SaveId;
@@ -415,7 +425,7 @@ namespace Core.Lifecycle
try
{
string serializedData = component.InvokeGlobalSaveRequested();
string serializedData = component.OnGlobalSaveRequested();
if (!string.IsNullOrEmpty(serializedData))
{
saveData[component.SaveId] = serializedData;
@@ -455,7 +465,7 @@ namespace Core.Lifecycle
{
try
{
component.InvokeSceneRestoreRequested(serializedData);
component.OnSceneRestoreRequested(serializedData);
restoredCount++;
LogDebug($"Restored scene data to: {component.SaveId}");
}
@@ -486,7 +496,7 @@ namespace Core.Lifecycle
try
{
component.InvokeSceneRestoreCompleted();
component.OnSceneRestoreCompleted();
}
catch (Exception ex)
{
@@ -519,7 +529,7 @@ namespace Core.Lifecycle
{
try
{
component.InvokeGlobalRestoreRequested(serializedData);
component.OnGlobalRestoreRequested(serializedData);
restoredCount++;
LogDebug($"Restored global data to: {component.SaveId}");
}
@@ -551,7 +561,7 @@ namespace Core.Lifecycle
try
{
component.InvokeGlobalLoadCompleted();
component.OnGlobalLoadCompleted();
}
catch (Exception ex)
{
@@ -578,7 +588,7 @@ namespace Core.Lifecycle
try
{
component.InvokeGlobalSaveStarted();
component.OnGlobalSaveStarted();
}
catch (Exception ex)
{

View File

@@ -1,5 +1,4 @@
using System;
using UnityEngine;
using UnityEngine;
namespace Core.Lifecycle
{
@@ -12,7 +11,7 @@ namespace Core.Lifecycle
#region Priority Properties
/// <summary>
/// Priority for OnManagedAwake (lower values execute first).
/// Priority for OnManagedStart (lower values execute first).
/// Default: 100
/// </summary>
public virtual int ManagedAwakePriority => 100;
@@ -81,23 +80,6 @@ namespace Core.Lifecycle
#endregion
#region Public Accessors (for LifecycleManager)
// Public wrappers to invoke protected lifecycle methods
public void InvokeManagedAwake() => OnManagedAwake();
public void InvokeSceneUnloading() => OnSceneUnloading();
public void InvokeSceneReady() => OnSceneReady();
public string InvokeSceneSaveRequested() => OnSceneSaveRequested();
public void InvokeSceneRestoreRequested(string data) => OnSceneRestoreRequested(data);
public void InvokeSceneRestoreCompleted() => OnSceneRestoreCompleted();
public string InvokeGlobalSaveRequested() => OnGlobalSaveRequested();
public void InvokeGlobalRestoreRequested(string data) => OnGlobalRestoreRequested(data);
public void InvokeManagedDestroy() => OnManagedDestroy();
public void InvokeGlobalLoadCompleted() => OnGlobalLoadCompleted();
public void InvokeGlobalSaveStarted() => OnGlobalSaveStarted();
#endregion
#region Private Fields
private bool _isRegistered;
@@ -108,9 +90,9 @@ namespace Core.Lifecycle
/// <summary>
/// Unity Awake - automatically registers with LifecycleManager.
/// IMPORTANT: Derived classes that override Awake MUST call base.Awake()
/// SEALED: Cannot be overridden. Use OnManagedAwake() for early initialization or OnManagedStart() for late initialization.
/// </summary>
protected virtual void Awake()
private void Awake()
{
if (LifecycleManager.Instance != null)
{
@@ -152,14 +134,27 @@ namespace Core.Lifecycle
#region Managed Lifecycle Hooks
/// <summary>
/// Called immediately during registration (during Awake).
/// Use for early initialization such as setting singleton instances.
/// TIMING: Fires during component's Awake(), no execution order guarantees between components.
/// NOT priority-ordered - fires whenever Unity calls this component's Awake().
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
internal virtual void OnManagedAwake()
{
// Override in derived classes
}
/// <summary>
/// Called once per component after bootstrap completes.
/// GUARANTEE: Bootstrap resources are available, all managers are initialized.
/// For boot-time components: Called during LifecycleManager.BroadcastManagedAwake (priority ordered).
/// For boot-time components: Called during LifecycleManager.BroadcastManagedStart (priority ordered).
/// For late-registered components: Called immediately upon registration (bootstrap already complete).
/// Replaces the old Awake + InitializePostBoot pattern.
/// Use for initialization that depends on other systems.
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnManagedAwake()
internal virtual void OnManagedStart()
{
// Override in derived classes
}
@@ -168,8 +163,9 @@ namespace Core.Lifecycle
/// Called before the scene this component belongs to is unloaded.
/// Called in REVERSE priority order (higher values execute first).
/// Use for scene-specific cleanup.
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnSceneUnloading()
internal virtual void OnSceneUnloading()
{
// Override in derived classes
}
@@ -178,8 +174,9 @@ namespace Core.Lifecycle
/// Called after the scene this component belongs to has finished loading.
/// Called in priority order (lower values execute first).
/// Use for scene-specific initialization.
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnSceneReady()
internal virtual void OnSceneReady()
{
// Override in derived classes
}
@@ -193,8 +190,10 @@ namespace Core.Lifecycle
/// - Called BEFORE scene unload during scene transitions
/// - Frequency: Every scene transition
/// - Use for: Level progress, object positions, puzzle states
///
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual string OnSceneSaveRequested()
internal virtual string OnSceneSaveRequested()
{
return null; // Default: no data to save
}
@@ -211,8 +210,10 @@ namespace Core.Lifecycle
/// - Called AFTER scene load, during OnSceneReady phase
/// - Frequency: Every scene transition
/// - Use for: Restoring level progress, object positions, puzzle states
///
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnSceneRestoreRequested(string serializedData)
internal virtual void OnSceneRestoreRequested(string serializedData)
{
// Default: no-op
}
@@ -234,8 +235,10 @@ namespace Core.Lifecycle
/// COMMON PATTERN:
/// Use this to perform actions that depend on whether data was restored or not.
/// Example: Play one-time audio only if it hasn't been played before (_hasPlayed == false).
///
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnSceneRestoreCompleted()
internal virtual void OnSceneRestoreCompleted()
{
// Default: no-op
}
@@ -248,8 +251,10 @@ namespace Core.Lifecycle
/// - Called ONCE on game boot after save file is read
/// - NOT called during scene transitions
/// - Use for: Player inventory, unlocked features, card collections
///
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnGlobalRestoreRequested(string serializedData)
internal virtual void OnGlobalRestoreRequested(string serializedData)
{
// Default: no-op
}
@@ -263,8 +268,10 @@ namespace Core.Lifecycle
/// - Called ONCE before save file is written (on quit, manual save, etc.)
/// - NOT called during scene transitions
/// - Use for: Player inventory, unlocked features, card collections
///
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual string OnGlobalSaveRequested()
internal virtual string OnGlobalSaveRequested()
{
return null; // Default: no data to save
}
@@ -278,8 +285,10 @@ namespace Core.Lifecycle
/// - Called ONCE on game boot after all restore operations complete
/// - NOT called during scene transitions
/// - Use for: Triggering UI updates, broadcasting load events
///
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnGlobalLoadCompleted()
internal virtual void OnGlobalLoadCompleted()
{
// Default: no-op
}
@@ -293,8 +302,10 @@ namespace Core.Lifecycle
/// - Called ONCE before save file is written
/// - NOT called during scene transitions
/// - Use for: Final validation, cleanup operations
///
/// NOTE: Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnGlobalSaveStarted()
internal virtual void OnGlobalSaveStarted()
{
// Default: no-op
}
@@ -304,8 +315,9 @@ namespace Core.Lifecycle
/// Called in REVERSE priority order (higher values execute first).
/// NOTE: Most cleanup is automatic (managed events, auto-registrations).
/// Only override if you need custom cleanup logic.
/// Internal visibility allows LifecycleManager to call directly. Override in derived classes.
/// </summary>
protected virtual void OnManagedDestroy()
internal virtual void OnManagedDestroy()
{
// Override in derived classes
}

View File

@@ -1,17 +1,144 @@
namespace Core
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Core
{
/// <summary>
/// Centralized logging system with automatic class/method tagging and editor integration.
/// Broadcasts log entries to custom editor windows for filtering and analysis.
/// </summary>
public static class Logging
{
[System.Diagnostics.Conditional("ENABLE_LOG")]
public static void Debug(object message)
/// <summary>
/// Event fired when a new log entry is added. Subscribe to this in editor windows.
/// </summary>
public static event Action<LogEntry> OnLogEntryAdded;
// Store recent logs for late-subscriber editor windows (e.g., windows opened after play mode started)
private static readonly List<LogEntry> RecentLogs = new List<LogEntry>();
private const int MaxStoredLogs = 5000; // Prevent memory bloat
/// <summary>
/// Get all recent logs. Used by editor windows when they first open.
/// </summary>
public static IReadOnlyList<LogEntry> GetRecentLogs() => RecentLogs;
/// <summary>
/// Clear all stored logs. Useful for editor windows "Clear" button.
/// </summary>
public static void ClearLogs()
{
UnityEngine.Debug.Log(message);
RecentLogs.Clear();
}
[System.Diagnostics.Conditional("ENABLE_LOG")]
public static void Warning(object message)
public static void Debug(string message,
[CallerFilePath] string filePath = "",
[CallerMemberName] string memberName = "")
{
UnityEngine.Debug.LogWarning(message);
LogInternal(LogLevel.Debug, message, filePath, memberName);
}
[System.Diagnostics.Conditional("ENABLE_LOG")]
public static void Info(string message,
[CallerFilePath] string filePath = "",
[CallerMemberName] string memberName = "")
{
LogInternal(LogLevel.Info, message, filePath, memberName);
}
[System.Diagnostics.Conditional("ENABLE_LOG")]
public static void Warning(string message,
[CallerFilePath] string filePath = "",
[CallerMemberName] string memberName = "")
{
LogInternal(LogLevel.Warning, message, filePath, memberName);
}
public static void Error(string message,
[CallerFilePath] string filePath = "",
[CallerMemberName] string memberName = "")
{
LogInternal(LogLevel.Error, message, filePath, memberName);
}
private static void LogInternal(LogLevel level, string message, string filePath, string memberName)
{
string className = Path.GetFileNameWithoutExtension(filePath);
string formattedMessage = $"[{className}][{memberName}] {message}";
// Create log entry
var entry = new LogEntry(className, memberName, message, level, Time.realtimeSinceStartup);
// Store for late subscribers
RecentLogs.Add(entry);
if (RecentLogs.Count > MaxStoredLogs)
RecentLogs.RemoveAt(0);
// Broadcast to editor windows (editor-only, won't fire in builds)
OnLogEntryAdded?.Invoke(entry);
// Also log to Unity console
switch (level)
{
case LogLevel.Debug:
case LogLevel.Info:
UnityEngine.Debug.Log(formattedMessage);
break;
case LogLevel.Warning:
UnityEngine.Debug.LogWarning(formattedMessage);
break;
case LogLevel.Error:
UnityEngine.Debug.LogError(formattedMessage);
break;
}
}
}
/// <summary>
/// Represents a single log entry with class, method, message, level, and timestamp.
/// </summary>
public class LogEntry
{
public string ClassName { get; }
public string MethodName { get; }
public string Message { get; }
public LogLevel Level { get; }
public float Timestamp { get; }
public LogEntry(string className, string methodName, string message, LogLevel level, float timestamp)
{
ClassName = className;
MethodName = methodName;
Message = message;
Level = level;
Timestamp = timestamp;
}
/// <summary>
/// Formatted message with class and method tags.
/// Format: [ClassName][MethodName] Message
/// </summary>
public string FormattedMessage => $"[{ClassName}][{MethodName}] {Message}";
/// <summary>
/// Full formatted message with timestamp and level.
/// Format: [12.34s][Debug][ClassName][MethodName] Message
/// </summary>
public string FullFormattedMessage => $"[{Timestamp:F2}s][{Level}]{FormattedMessage}";
}
/// <summary>
/// Log severity levels.
/// </summary>
public enum LogLevel
{
Debug,
Info,
Warning,
Error
}
}

View File

@@ -129,20 +129,18 @@ namespace AppleHills.Core
#region Lifecycle Methods
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// QuickAccess has minimal initialization
}
protected override void OnSceneUnloading()
internal override void OnSceneUnloading()
{
// Clear references BEFORE scene unloads for better cleanup timing
ClearReferences();

View File

@@ -46,11 +46,9 @@ namespace Core.SaveLoad
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 20; // After GameManager and SceneManagerService
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Initialize critical state immediately
@@ -58,7 +56,7 @@ namespace Core.SaveLoad
IsRestoringState = false;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
Logging.Debug("[SaveLoadManager] Initialized");
@@ -69,19 +67,19 @@ namespace Core.SaveLoad
}
}
protected override void OnSceneReady()
internal override void OnSceneReady()
{
// SaveableInteractables now auto-register via ManagedBehaviour lifecycle
// No need to discover and register them manually
}
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
// SaveLoadManager orchestrates saves, doesn't participate in them
return null;
}
protected override string OnGlobalSaveRequested()
internal override string OnGlobalSaveRequested()
{
// SaveLoadManager orchestrates saves, doesn't participate in them
return null;

View File

@@ -30,9 +30,9 @@ namespace Core
// Enable save/load participation
public override bool AutoRegisterForSave => true;
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake();
base.OnManagedAwake();
_director = GetComponent<PlayableDirector>();
if (_director != null)
@@ -65,7 +65,7 @@ namespace Core
#region Save/Load Implementation
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
var saveData = new PlayableDirectorSaveData
{
@@ -77,7 +77,7 @@ namespace Core
return JsonUtility.ToJson(saveData);
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{

View File

@@ -47,11 +47,9 @@ namespace Core
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 15; // Core infrastructure, after GameManager
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Initialize current scene tracking - critical for scene management
@@ -65,17 +63,17 @@ namespace Core
}
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Set up loading screen reference and events
// This must happen in ManagedAwake because LoadingScreenController instance needs to be set first
// This must happen in ManagedStart because LoadingScreenController instance needs to be set first
_loadingScreen = LoadingScreenController.Instance;
SetupLoadingScreenEvents();
// Load verbosity settings
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
LogDebugMessage($"SceneManagerService initialized, current scene is: {CurrentGameplayScene}");
Logging.Debug($"SceneManagerService initialized, current scene is: {CurrentGameplayScene}");
}
/// <summary>
@@ -93,19 +91,19 @@ namespace Core
if (activeScene.name != BootstrapSceneName)
{
CurrentGameplayScene = activeScene.name;
LogDebugMessage($"Initialized with current scene: {CurrentGameplayScene}");
Logging.Debug($"Initialized with current scene: {CurrentGameplayScene}");
}
// Otherwise default to MainMenu
else
{
CurrentGameplayScene = "AppleHillsOverworld";
LogDebugMessage($"Initialized with default scene: {CurrentGameplayScene}");
Logging.Debug($"Initialized with default scene: {CurrentGameplayScene}");
}
}
else
{
CurrentGameplayScene = "AppleHillsOverworld";
LogDebugMessage($"No valid active scene, defaulting to: {CurrentGameplayScene}");
Logging.Debug($"No valid active scene, defaulting to: {CurrentGameplayScene}");
}
}
@@ -307,7 +305,7 @@ namespace Core
}
// PHASE 2: Broadcast scene unloading - notify components to cleanup
LogDebugMessage($"Broadcasting OnSceneUnloading for: {oldSceneName}");
Logging.Debug($"Broadcasting OnSceneUnloading for: {oldSceneName}");
LifecycleManager.Instance?.BroadcastSceneUnloading(oldSceneName);
// PHASE 3: Save scene-specific data via SaveLoadManager (unless skipSave is true)
@@ -316,19 +314,19 @@ namespace Core
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
LogDebugMessage($"Saving scene data for: {oldSceneName}");
Logging.Debug($"Saving scene data for: {oldSceneName}");
SaveLoadManager.Instance.SaveSceneData();
}
}
else if (skipSave)
{
LogDebugMessage($"Skipping save for: {oldSceneName} (skipSave=true)");
Logging.Debug($"Skipping save for: {oldSceneName} (skipSave=true)");
}
// PHASE 4: Clear PuzzleManager state before scene transition
if (PuzzleS.PuzzleManager.Instance != null)
{
LogDebugMessage($"Clearing puzzle state before scene transition");
Logging.Debug($"Clearing puzzle state before scene transition");
PuzzleS.PuzzleManager.Instance.ClearPuzzleState();
}
@@ -352,7 +350,7 @@ namespace Core
}
else
{
Logging.Warning($"[SceneManagerService] Previous scene '{oldSceneName}' is not loaded, skipping unload.");
Logging.Warning($"Previous scene '{oldSceneName}' is not loaded, skipping unload.");
}
}
@@ -364,7 +362,7 @@ namespace Core
}
// PHASE 8: Begin scene loading mode - enables priority-ordered component initialization
LogDebugMessage($"Beginning scene load for: {newSceneName}");
Logging.Debug($"Beginning scene load for: {newSceneName}");
LifecycleManager.Instance?.BeginSceneLoad(newSceneName);
// PHASE 9: Load new gameplay scene
@@ -372,7 +370,7 @@ namespace Core
CurrentGameplayScene = newSceneName;
// PHASE 10: Broadcast scene ready - processes batched components in priority order, then calls OnSceneReady
LogDebugMessage($"Broadcasting OnSceneReady for: {newSceneName}");
Logging.Debug($"Broadcasting OnSceneReady for: {newSceneName}");
LifecycleManager.Instance?.BroadcastSceneReady(newSceneName);
// PHASE 11: Restore scene-specific data via SaveLoadManager
@@ -381,14 +379,14 @@ namespace Core
var debugSettings = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>();
if (debugSettings.useSaveLoadSystem)
{
LogDebugMessage($"Restoring scene data for: {newSceneName}");
Logging.Debug($"Restoring scene data for: {newSceneName}");
SaveLoadManager.Instance.RestoreSceneData();
}
}
else if (skipSave)
{
SaveLoadManager.Instance.RestoreSceneData();
LogDebugMessage($"Skipping restore for: {newSceneName} (skipSave=true)");
Logging.Debug($"Skipping restore for: {newSceneName} (skipSave=true)");
}
// PHASE 12: Only hide the loading screen if autoHideLoadingScreen is true
@@ -397,13 +395,5 @@ namespace Core
_loadingScreen.HideLoadingScreen();
}
}
private void LogDebugMessage(string message)
{
if (_logVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[SceneManagerService] {message}");
}
}
}
}

View File

@@ -21,20 +21,18 @@ namespace Core
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 70; // Platform-specific utility
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Load verbosity settings early (GameManager sets up settings in its Awake)
// Load verbosity settings early (GameManager sets up settings in its OnManagedAwake)
_logVerbosity = DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().sceneLogVerbosity;
LogDebugMessage("Initialized");
Logging.Debug("Initialized");
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Subscribe to SceneManagerService to enforce orientation on every scene load
if (SceneManagerService.Instance != null)
@@ -51,7 +49,7 @@ namespace Core
#endif
}
protected override void OnSceneReady()
internal override void OnSceneReady()
{
// Handle orientation when scene is ready (initial scene)
// Note: This fires for the scene that just loaded, LifecycleManager tracks which scene
@@ -73,7 +71,7 @@ namespace Core
if (sceneName.ToLower().Contains("bootstrap"))
{
// Bootstrap being loaded additively, don't do anything
LogDebugMessage($"Detected bootstrapped scene: '{sceneName}'. Skipping orientation enforcement.");
Logging.Debug($"Detected bootstrapped scene: '{sceneName}'. Skipping orientation enforcement.");
return;
}
@@ -83,23 +81,23 @@ namespace Core
}
else
{
LogDebugMessage($"No orientationConfig assigned. Defaulting to Landscape for scene '{sceneName}'");
Logging.Debug($"No orientationConfig assigned. Defaulting to Landscape for scene '{sceneName}'");
}
switch (requirement)
{
case ScreenOrientationRequirement.Portrait:
LogDebugMessage($"Forcing Portrait for scene '{sceneName}'");
Logging.Debug($"Forcing Portrait for scene '{sceneName}'");
StartCoroutine(ForcePortrait());
break;
case ScreenOrientationRequirement.Landscape:
LogDebugMessage($"Forcing Landscape for scene '{sceneName}'");
Logging.Debug($"Forcing Landscape for scene '{sceneName}'");
StartCoroutine(ForceLandscape());
break;
case ScreenOrientationRequirement.NotApplicable:
default:
// Default to landscape when no specific requirement is found
LogDebugMessage($"No specific orientation for scene '{sceneName}'. Defaulting to Landscape");
Logging.Debug($"No specific orientation for scene '{sceneName}'. Defaulting to Landscape");
StartCoroutine(ForceLandscape());
break;
}
@@ -127,12 +125,12 @@ namespace Core
if (!currentlyLandscape)
{
// Lock it to portrait and allow the device to orient itself
LogDebugMessage($"Actually forcing Portrait from previous: {Screen.orientation}");
Logging.Debug($"Actually forcing Portrait from previous: {Screen.orientation}");
Screen.orientation = ScreenOrientation.LandscapeRight;
}
else
{
LogDebugMessage($"Skipping Landscape enforcement, device already in: {Screen.orientation}");
Logging.Debug($"Skipping Landscape enforcement, device already in: {Screen.orientation}");
}
yield return null;
@@ -160,12 +158,12 @@ namespace Core
if (!currentlyPortrait)
{
// Lock it to portrait and allow the device to orient itself
LogDebugMessage($"Actually forcing Portrait from previous: {Screen.orientation}");
Logging.Debug($"Actually forcing Portrait from previous: {Screen.orientation}");
Screen.orientation = ScreenOrientation.PortraitUpsideDown;
}
else
{
LogDebugMessage($"Skipping Portrait enforcement, device already in: {Screen.orientation}");
Logging.Debug($"Skipping Portrait enforcement, device already in: {Screen.orientation}");
}
yield return null;
@@ -181,13 +179,5 @@ namespace Core
// Allow device to auto-rotate to correct portrait orientation
Screen.orientation = ScreenOrientation.AutoRotation;
}
private void LogDebugMessage(string message)
{
if (_logVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[SceneOrientationEnforcer] {message}");
}
}
}
}

View File

@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using AppleHills.Core.Settings;
using UnityEngine;
namespace Core.Settings
{
@@ -21,7 +19,7 @@ namespace Core.Settings
public static void Register<T>(T service) where T : class
{
Services[typeof(T)] = service;
LogDebugMessage($"Service registered: {typeof(T).Name}");
Logging.Debug($"Service registered: {typeof(T).Name}");
}
/// <summary>
@@ -36,7 +34,7 @@ namespace Core.Settings
return service as T;
}
Logging.Warning($"[ServiceLocator] Service of type {typeof(T).Name} not found!");
Logging.Warning($"Service of type {typeof(T).Name} not found!");
return null;
}
@@ -46,16 +44,7 @@ namespace Core.Settings
public static void Clear()
{
Services.Clear();
LogDebugMessage("All services cleared");
}
private static void LogDebugMessage(string message)
{
if (DeveloperSettingsProvider.Instance.GetSettings<DebugSettings>().settingsLogVerbosity <=
LogVerbosity.Debug)
{
Logging.Debug($"[ServiceLocator] {message}");
}
Logging.Debug("All services cleared");
}
}
}

View File

@@ -1,11 +1,12 @@
using UnityEngine;
using Pixelplacement;
using System;
using System.Collections;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
using UnityEngine.Audio;
public class PicnicBehaviour : MonoBehaviour
public class PicnicBehaviour : ManagedBehaviour
{
[Header("Random Call Settings")]
public float getDistractedMin = 2f;
@@ -17,7 +18,8 @@ public class PicnicBehaviour : MonoBehaviour
private Animator animator;
[Header("The FakeChocolate to destroy!")]
[SerializeField] private GameObject fakeChocolate; // Assign in Inspector
[SerializeField] private GameObject fakeChocolate;
[SerializeField] private GameObject realChocolate;
private AppleAudioSource _audioSource;
public AudioResource distractedAudioClips;
@@ -25,32 +27,44 @@ public class PicnicBehaviour : MonoBehaviour
public AudioResource feederClips;
public AudioResource moanerClips;
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
StartCoroutine(StateCycleRoutine());
}
// Save system configuration
public override bool AutoRegisterForSave => true;
void Awake()
// Runtime state tracking
private bool _fakeChocolateDestroyed;
internal override void OnManagedAwake()
{
stateMachine = GetComponent<AppleMachine>();
animator = GetComponent<Animator>();
_audioSource = GetComponent<AppleAudioSource>();
}
internal override void OnSceneRestoreCompleted()
{
if (_fakeChocolateDestroyed)
{
DestroyChocolateObjects();
}
else
{
StartCoroutine(StateCycleRoutine());
}
}
private IEnumerator StateCycleRoutine()
{
while (true)
{
// Distracted state
float distractedWait = Random.Range(getDistractedMin, getDistractedMax);
float distractedWait = UnityEngine.Random.Range(getDistractedMin, getDistractedMax);
stateMachine.ChangeState("Picnic PPL Distracted");
animator.SetBool("theyDistracted", true);
_audioSource.Stop();
yield return new WaitForSeconds(distractedWait);
// Chilling state
float chillingWait = Random.Range(getFlirtyMin, getFlirtyMax);
float chillingWait = UnityEngine.Random.Range(getFlirtyMin, getFlirtyMax);
stateMachine.ChangeState("Picnic PPL Chilling");
animator.SetBool("theyDistracted", false);
_audioSource.Stop();
@@ -58,27 +72,33 @@ public class PicnicBehaviour : MonoBehaviour
}
}
void StopAudio()
{
_audioSource.Stop();
}
public void triedToStealChocolate()
{
_audioSource.Stop();
animator.SetTrigger("theyAngry");
//stateMachine.ChangeState("Picnic PPL Angry");
Logging.Debug("Hey! Don't steal my chocolate!");
_audioSource.audioSource.resource = angryAudioClips;
_audioSource.Play(0);
}
public void destroyFakeChocolate()
{
_fakeChocolateDestroyed = true;
Destroy(fakeChocolate);
Destroy(realChocolate);
}
private void DestroyChocolateObjects()
{
if (fakeChocolate != null)
{
Destroy(fakeChocolate);
fakeChocolate = null; // Optional: clear reference
fakeChocolate = null;
}
if (realChocolate != null)
{
realChocolate.SetActive(true);
}
}
@@ -100,5 +120,33 @@ public class PicnicBehaviour : MonoBehaviour
_audioSource.Play(0);
}
internal override string OnSceneSaveRequested()
{
var state = new PicnicBehaviourState { fakeChocolateDestroyed = _fakeChocolateDestroyed };
return JsonUtility.ToJson(state);
}
internal override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData)) return;
try
{
var state = JsonUtility.FromJson<PicnicBehaviourState>(serializedData);
if (state != null)
{
_fakeChocolateDestroyed = state.fakeChocolateDestroyed;
}
}
catch (Exception ex)
{
Debug.LogWarning($"[PicnicBehaviour] Failed to restore state: {ex.Message}");
}
}
}
[Serializable]
public class PicnicBehaviourState
{
public bool fakeChocolateDestroyed;
}

View File

@@ -22,7 +22,7 @@ public class soundBird_CanFly : ManagedBehaviour
#region Save/Load Implementation
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
var saveData = new SoundBirdSaveData
{
@@ -32,7 +32,7 @@ public class soundBird_CanFly : ManagedBehaviour
return JsonUtility.ToJson(saveData);
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{

View File

@@ -40,25 +40,22 @@ namespace Data.CardSystem
// Event callbacks using System.Action
public event Action<List<CardData>> OnBoosterOpened;
public event Action<CardData> OnCardCollected;
public event Action<CardData> OnCardRarityUpgraded;
public event Action<int> OnBoosterCountChanged;
public event Action<CardData> OnPendingCardAdded;
public event Action<CardData> OnCardPlacedInAlbum;
public override int ManagedAwakePriority => 60; // Data systems
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Load card definitions from Addressables, then register with save system
LoadCardDefinitionsFromAddressables();
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
Logging.Debug("[CardSystemManager] Initialized");
}
@@ -720,13 +717,13 @@ namespace Data.CardSystem
#region Save/Load Lifecycle Hooks
protected override string OnGlobalSaveRequested()
internal override string OnGlobalSaveRequested()
{
var state = ExportCardCollectionState();
return JsonUtility.ToJson(state);
}
protected override void OnGlobalRestoreRequested(string serializedData)
internal override void OnGlobalRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Generic;
using Core;
using Core.Lifecycle;
using Interactions;
@@ -37,7 +35,7 @@ namespace Dialogue
public override int ManagedAwakePriority => 150; // Dialogue systems
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Get required components
appleAudioSource = GetComponent<AppleAudioSource>();
@@ -186,8 +184,10 @@ namespace Dialogue
return null;
}
private void OnDestroy()
protected override void OnDestroy()
{
base.OnDestroy();
// Unregister from events
if (PuzzleManager.Instance != null)
PuzzleManager.Instance.OnStepCompleted -= OnAnyPuzzleStepCompleted;

View File

@@ -52,11 +52,9 @@ namespace Input
public override int ManagedAwakePriority => 25; // Input infrastructure
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Load verbosity settings early
@@ -89,10 +87,10 @@ namespace Input
SwitchInputOnSceneLoaded(SceneManager.GetActiveScene().name);
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Subscribe to scene load events from SceneManagerService
// This must happen in ManagedAwake because SceneManagerService instance needs to be set first
// This must happen in ManagedStart because SceneManagerService instance needs to be set first
if (SceneManagerService.Instance != null)
{
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
@@ -104,7 +102,7 @@ namespace Input
/// </summary>
private void OnSceneLoadCompleted(string sceneName)
{
LogDebugMessage($"Scene loaded: {sceneName}, restoring input mode");
Logging.Debug($"Scene loaded: {sceneName}, restoring input mode");
SwitchInputOnSceneLoaded(sceneName);
}
@@ -182,24 +180,24 @@ namespace Input
Vector2 screenPos = positionAction.ReadValue<Vector2>();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
LogDebugMessage($"TapMove performed at {worldPos2D}");
Logging.Debug($"TapMove performed at {worldPos2D}");
// First try to delegate to an override consumer if available
if (TryDelegateToOverrideConsumer(screenPos, worldPos2D))
{
LogDebugMessage("Tap delegated to override consumer");
Logging.Debug("Tap delegated to override consumer");
return;
}
// Then try to delegate to any ITouchInputConsumer (UI or world interactable)
if (!TryDelegateToAnyInputConsumer(screenPos, worldPos2D))
{
LogDebugMessage("No input consumer found, forwarding tap to default consumer");
Logging.Debug("No input consumer found, forwarding tap to default consumer");
defaultConsumer?.OnTap(worldPos2D);
}
else
{
LogDebugMessage("Tap delegated to input consumer");
Logging.Debug("Tap delegated to input consumer");
}
}
@@ -212,13 +210,13 @@ namespace Input
Vector2 screenPos = positionAction.ReadValue<Vector2>();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
LogDebugMessage($"HoldMove started at {worldPos2D}");
Logging.Debug($"HoldMove started at {worldPos2D}");
// First check for override consumers
if (_overrideConsumers.Count > 0)
{
_activeHoldConsumer = _overrideConsumers[_overrideConsumers.Count - 1];
LogDebugMessage($"Hold delegated to override consumer: {_activeHoldConsumer}");
Logging.Debug($"Hold delegated to override consumer: {_activeHoldConsumer}");
_activeHoldConsumer.OnHoldStart(worldPos2D);
return;
}
@@ -238,7 +236,7 @@ namespace Input
Vector2 screenPos = positionAction.ReadValue<Vector2>();
Vector3 worldPos = Camera.main.ScreenToWorldPoint(screenPos);
Vector2 worldPos2D = new Vector2(worldPos.x, worldPos.y);
LogDebugMessage($"HoldMove canceled at {worldPos2D}");
Logging.Debug($"HoldMove canceled at {worldPos2D}");
// Notify the active hold consumer that the hold has ended
_activeHoldConsumer?.OnHoldEnd(worldPos2D);
@@ -302,7 +300,7 @@ namespace Input
}
if (consumer != null)
{
LogDebugMessage($"Delegating tap to UI consumer at {screenPos} (GameObject: {result.gameObject.name})");
Logging.Debug($"Delegating tap to UI consumer at {screenPos} (GameObject: {result.gameObject.name})");
consumer.OnTap(screenPos);
return true;
}
@@ -331,7 +329,7 @@ namespace Input
}
if (consumer != null)
{
LogDebugMessage($"Delegating tap to consumer at {worldPos} (GameObject: {hitWithMask.gameObject.name})");
Logging.Debug($"Delegating tap to consumer at {worldPos} (GameObject: {hitWithMask.gameObject.name})");
consumer.OnTap(worldPos);
return true;
}
@@ -345,15 +343,15 @@ namespace Input
var consumer = hit.GetComponent<ITouchInputConsumer>();
if (consumer != null)
{
LogDebugMessage($"Delegating tap to consumer at {worldPos} (GameObject: {hit.gameObject.name})");
Logging.Debug($"Delegating tap to consumer at {worldPos} (GameObject: {hit.gameObject.name})");
consumer.OnTap(worldPos);
return true;
}
LogDebugMessage($"Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found.");
Logging.Debug($"Collider2D hit at {worldPos} (GameObject: {hit.gameObject.name}), but no ITouchInputConsumer found.");
}
else
{
LogDebugMessage($"No Collider2D found at {worldPos} for interactable delegation.");
Logging.Debug($"No Collider2D found at {worldPos} for interactable delegation.");
}
return false;
}
@@ -368,7 +366,7 @@ namespace Input
return;
_overrideConsumers.Add(consumer);
LogDebugMessage($"Override consumer registered: {consumer}");
Logging.Debug($"Override consumer registered: {consumer}");
}
/// <summary>
@@ -386,7 +384,7 @@ namespace Input
}
_overrideConsumers.Remove(consumer);
LogDebugMessage($"Override consumer unregistered: {consumer}");
Logging.Debug($"Override consumer unregistered: {consumer}");
}
/// <summary>
@@ -396,7 +394,7 @@ namespace Input
{
_activeHoldConsumer = null;
_overrideConsumers.Clear();
LogDebugMessage("All override consumers cleared.");
Logging.Debug("All override consumers cleared.");
}
/// <summary>
@@ -409,17 +407,9 @@ namespace Input
// Get the topmost override consumer (last registered)
var consumer = _overrideConsumers[_overrideConsumers.Count - 1];
LogDebugMessage($"Delegating tap to override consumer at {worldPos} (GameObject: {consumer})");
Logging.Debug($"Delegating tap to override consumer at {worldPos} (GameObject: {consumer})");
consumer.OnTap(worldPos);
return true;
}
private void LogDebugMessage(string message)
{
if (_logVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[InputManager] {message}");
}
}
}
}

View File

@@ -3,7 +3,6 @@ using Pathfinding;
using AppleHills.Core.Settings;
using Core;
using Core.Lifecycle;
using Core.SaveLoad;
namespace Input
{
@@ -73,7 +72,7 @@ namespace Input
public override string SaveId => $"{gameObject.scene.name}/PlayerController";
public override int ManagedAwakePriority => 100; // Player controller
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
aiPath = GetComponent<AIPath>();
artTransform = transform.Find("CharacterArt");
@@ -103,7 +102,7 @@ namespace Input
public void OnTap(Vector2 worldPosition)
{
InterruptMoveTo();
LogDebugMessage($"OnTap at {worldPosition}");
Logging.Debug($"OnTap at {worldPosition}");
if (aiPath != null)
{
aiPath.enabled = true;
@@ -122,7 +121,7 @@ namespace Input
public void OnHoldStart(Vector2 worldPosition)
{
InterruptMoveTo();
LogDebugMessage($"OnHoldStart at {worldPosition}");
Logging.Debug($"OnHoldStart at {worldPosition}");
lastHoldPosition = worldPosition;
isHolding = true;
if (_settings.DefaultHoldMovementMode == HoldMovementMode.Pathfinding &&
@@ -159,7 +158,7 @@ namespace Input
/// </summary>
public void OnHoldEnd(Vector2 worldPosition)
{
LogDebugMessage($"OnHoldEnd at {worldPosition}");
Logging.Debug($"OnHoldEnd at {worldPosition}");
isHolding = false;
directMoveVelocity = Vector3.zero;
if (aiPath != null && _settings.DefaultHoldMovementMode ==
@@ -335,13 +334,13 @@ namespace Input
{
_isMoving = true;
OnMovementStarted?.Invoke();
LogDebugMessage("Movement started");
Logging.Debug("Movement started");
}
else if (!isCurrentlyMoving && _isMoving)
{
_isMoving = false;
OnMovementStopped?.Invoke();
LogDebugMessage("Movement stopped");
Logging.Debug("Movement stopped");
}
}
@@ -424,18 +423,10 @@ namespace Input
OnArrivedAtTarget?.Invoke();
}
}
private void LogDebugMessage(string message)
{
if (_logVerbosity <= LogVerbosity.Debug)
{
Logging.Debug($"[PlayerTouchController] {message}");
}
}
#region Save/Load Lifecycle Hooks
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
var saveData = new PlayerSaveData
{
@@ -445,7 +436,7 @@ namespace Input
return JsonUtility.ToJson(saveData);
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{

View File

@@ -68,8 +68,6 @@ namespace Interactions
public event Action<PickupItemData, PickupItemData> OnIncorrectItemSlotted;
public UnityEvent onForbiddenItemSlotted;
// Native C# event alternative for code-only subscribers
public event Action<PickupItemData, PickupItemData> OnForbiddenItemSlotted;
public GameObject GetSlottedObject()
{
@@ -85,9 +83,9 @@ namespace Interactions
}
}
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // SaveableInteractable registration
base.OnManagedAwake(); // SaveableInteractable registration
// Setup visuals
if (iconRenderer == null)

View File

@@ -32,9 +32,9 @@ namespace Interactions
public event Action<PickupItemData> OnItemPickedUp;
public event Action<PickupItemData, PickupItemData, PickupItemData> OnItemsCombined;
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // Register with save system
base.OnManagedAwake(); // Register with save system
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
@@ -44,8 +44,9 @@ namespace Interactions
// Always register with ItemManager, even if picked up
// This allows the save/load system to find held items when restoring state
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
base.OnManagedStart();
ItemManager.Instance?.RegisterPickup(this);
}

View File

@@ -24,7 +24,7 @@ namespace Interactions
#region Save/Load Lifecycle Hooks
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
object stateData = GetSerializableState();
if (stateData == null)
@@ -35,7 +35,7 @@ namespace Interactions
return JsonUtility.ToJson(stateData);
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{

View File

@@ -21,9 +21,9 @@ namespace Levels
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake();
base.OnManagedAwake();
Logging.Debug($"[LevelSwitch] Awake called for {gameObject.name} in scene {gameObject.scene.name}");
@@ -36,12 +36,12 @@ namespace Levels
ApplySwitchData();
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
Logging.Debug($"[LevelSwitch] OnManagedAwake called for {gameObject.name}");
Logging.Debug($"[LevelSwitch] OnManagedStart called for {gameObject.name}");
}
protected override void OnSceneReady()
internal override void OnSceneReady()
{
Logging.Debug($"[LevelSwitch] OnSceneReady called for {gameObject.name}");
}

View File

@@ -35,21 +35,16 @@ namespace Levels
[SerializeField] private bool startUnlocked = false;
private SpriteRenderer iconRenderer;
// Settings reference
private IInteractionSettings interactionSettings;
private bool switchActive = true;
private bool isUnlocked;
/// <summary>
/// Unity Awake callback. Sets up icon, interactable, and event handlers.
/// </summary>
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake();
switchActive = true;
base.OnManagedAwake();
if (iconRenderer == null)
iconRenderer = GetComponent<SpriteRenderer>();
@@ -64,10 +59,9 @@ namespace Levels
ApplySwitchData();
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
base.OnManagedAwake();
base.OnManagedStart();
// If startUnlocked is true, always start active
if (startUnlocked)
{
@@ -144,15 +138,7 @@ namespace Levels
{
return base.CanBeClicked() && isUnlocked;
}
/// <summary>
/// Setup: Prevent re-entry while interaction is in progress.
/// </summary>
protected override void OnInteractionStarted()
{
switchActive = false;
}
/// <summary>
/// Main interaction logic: Spawn menu and switch input mode.
/// </summary>
@@ -203,7 +189,7 @@ namespace Levels
private void OnMenuCancel()
{
switchActive = true; // Allow interaction again if cancelled
InputManager.Instance.SetInputMode(InputMode.GameAndUI);
}

View File

@@ -107,10 +107,8 @@ namespace Minigames.DivingForPictures
public override int ManagedAwakePriority => 190;
public override bool AutoRegisterPausable => true; // Automatic GameManager registration
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake();
if (_instance == null)
{
_instance = this;
@@ -121,7 +119,7 @@ namespace Minigames.DivingForPictures
}
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
_settings = GameManager.GetSettingsObject<IDivingMinigameSettings>();
_currentSpawnProbability = _settings?.BaseSpawnProbability ?? 0.2f;
@@ -132,7 +130,7 @@ namespace Minigames.DivingForPictures
Logging.Debug("[DivingGameManager] Initialized");
}
protected override void OnSceneReady()
internal override void OnSceneReady()
{
InitializeGame();

View File

@@ -108,7 +108,7 @@ public class FollowerController : ManagedBehaviour
public override int ManagedAwakePriority => 110; // Follower after player
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
_aiPath = GetComponent<AIPath>();
// Find art prefab and animator
@@ -129,7 +129,7 @@ public class FollowerController : ManagedBehaviour
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
}
protected override void OnSceneReady()
internal override void OnSceneReady()
{
// Find player reference when scene is ready (called for every scene load)
FindPlayerReference();
@@ -727,7 +727,7 @@ public class FollowerController : ManagedBehaviour
#region Save/Load Lifecycle Hooks
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
var saveData = new FollowerSaveData
{
@@ -754,7 +754,7 @@ public class FollowerController : ManagedBehaviour
return JsonUtility.ToJson(saveData);
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{
@@ -914,7 +914,7 @@ public class FollowerController : ManagedBehaviour
/// </summary>
public static FollowerController FindInstance()
{
return FindObjectOfType<FollowerController>();
return FindFirstObjectByType<FollowerController>();
}
#endregion Save/Load Lifecycle Hooks

View File

@@ -16,7 +16,7 @@ namespace PuzzleS
// Save system configuration
public override bool AutoRegisterForSave => true;
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Initialize after all managers are ready
}
@@ -28,7 +28,7 @@ namespace PuzzleS
#region Save/Load Lifecycle Hooks
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
// Save scene-specific progress
var state = new BirdGameState
@@ -38,7 +38,7 @@ namespace PuzzleS
return JsonUtility.ToJson(state);
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
if (string.IsNullOrEmpty(serializedData))
{

View File

@@ -32,7 +32,7 @@ namespace PuzzleS
// Enum for tracking proximity state (simplified to just Close and Far)
public enum ProximityState { Close, Far }
protected override void Awake()
internal override void OnManagedAwake()
{
_interactable = GetComponent<InteractableBase>();
@@ -56,14 +56,10 @@ namespace PuzzleS
Logging.Warning($"[Puzzles] Indicator prefab for {stepData?.stepId} does not implement IPuzzlePrompt");
}
}
base.Awake();
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
base.OnManagedAwake();
// Register with PuzzleManager - safe to access .Instance here
if (stepData != null && PuzzleManager.Instance != null)
{

View File

@@ -96,15 +96,13 @@ namespace PuzzleS
public override int ManagedAwakePriority => 80; // Puzzle systems
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Initialize settings reference
_interactionSettings = GameManager.GetSettingsObject<IInteractionSettings>();
@@ -591,7 +589,7 @@ namespace PuzzleS
#region Save/Load Lifecycle Hooks
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
if (_currentLevelData == null)
{
@@ -611,7 +609,7 @@ namespace PuzzleS
return json;
}
protected override void OnSceneRestoreRequested(string data)
internal override void OnSceneRestoreRequested(string data)
{
Logging.Debug("[XAXA] PuzzleManager loading with data: " + data);

View File

@@ -20,15 +20,13 @@ public class AppleAudioSource : ManagedBehaviour
public int sourcePriority;
protected override void Awake()
internal override void OnManagedAwake()
{
base.Awake();
audioSource = GetComponent<AudioSource>();
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
audioSource = GetComponent<AudioSource>();
AudioManager.Instance.RegisterNewAudioSource(this);
_audioMixer = AudioManager.Instance.audioMixer;
InitializeAudioSource();

View File

@@ -44,15 +44,13 @@ public class AudioManager : ManagedBehaviour, IPausable
public override int ManagedAwakePriority => 30; // Audio infrastructure
public override bool AutoRegisterPausable => true; // Auto-register as IPausable
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Initialize lists if they were not set in inspector
criticalVOSources = criticalVOSources ?? new List<AppleAudioSource>();
@@ -72,7 +70,7 @@ public class AudioManager : ManagedBehaviour, IPausable
}
else
{
Logging.Warning("[AudioManager] QuickAccess.Instance is null during OnManagedAwake. Some audio references may remain unset.");
Logging.Warning("[AudioManager] QuickAccess.Instance is null during OnManagedStart. Some audio references may remain unset.");
}
// Diagnostic

View File

@@ -23,7 +23,7 @@ public class BushAudioController : ManagedBehaviour
// Start is called once before the first execution of Update after the MonoBehaviour is created
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
_eventSource = VOPlayer.audioSource.RequestEventHandlers();
_eventSource.AudioStopped += PlayBirdCounter;

View File

@@ -26,7 +26,7 @@ public class LevelAudioObject : ManagedBehaviour
public override bool AutoRegisterForSave => isOneTime; // Only save if one-time audio
protected override string OnSceneSaveRequested()
internal override string OnSceneSaveRequested()
{
if (!isOneTime)
return null; // No need to save if not one-time
@@ -39,7 +39,7 @@ public class LevelAudioObject : ManagedBehaviour
return JsonUtility.ToJson(saveData);
}
protected override void OnSceneRestoreRequested(string serializedData)
internal override void OnSceneRestoreRequested(string serializedData)
{
if (!isOneTime || string.IsNullOrEmpty(serializedData))
return;
@@ -55,7 +55,7 @@ public class LevelAudioObject : ManagedBehaviour
}
}
protected override void OnSceneRestoreCompleted()
internal override void OnSceneRestoreCompleted()
{
if (isOneTime && !_hasPlayed)
{

View File

@@ -10,7 +10,7 @@ public class PulverAudioController : ManagedBehaviour
private FollowerController followerController;
public ItemManager itemManager;
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
followerController = GetComponent<FollowerController>();
followerController.PulverIsCombining.AddListener(PulverIsCombining);

View File

@@ -25,10 +25,10 @@ public class AppSwitcher : UIPage
private TweenBase slideInTween;
private TweenBase slideOutTween;
protected override void OnManagedAwake()
internal override void OnManagedAwake()
{
base.OnManagedAwake();
PageName = "AppSwitcher";
rainbowInPlayer = rainbowIn.GetComponent<SkottiePlayerV2>();
rainbowOutPlayer = rainbowOut.GetComponent<SkottiePlayerV2>();

View File

@@ -44,10 +44,8 @@ namespace UI.CardSystem
private List<AlbumCardPlacementDraggable> _activeCards = new List<AlbumCardPlacementDraggable>();
private const int MAX_VISIBLE_CARDS = 3;
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
base.OnManagedAwake();
// Discover zone tabs from container
DiscoverZoneTabs();

View File

@@ -40,10 +40,8 @@ namespace UI.CardSystem
private TweenBase _activeTween;
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
base.OnManagedAwake();
// Store original scale for pulse animation
if (dotBackground != null)
{

View File

@@ -34,7 +34,6 @@ namespace UI.CardSystem
[SerializeField] private float cardSpacing = 150f;
[Header("Settings")]
[SerializeField] private float cardRevealDelay = 0.5f;
[SerializeField] private float boosterDisappearDuration = 0.5f;
[SerializeField] private CinemachineImpulseSource impulseSource;
[SerializeField] private ParticleSystem openingParticleSystem;
@@ -77,8 +76,10 @@ namespace UI.CardSystem
gameObject.SetActive(false);
}
private void OnDestroy()
protected override void OnDestroy()
{
base.OnDestroy();
// Unsubscribe from dismiss button
if (_dismissButton != null)
{

View File

@@ -73,7 +73,7 @@ namespace UI.CardSystem
albumCard.SetParentSlot(this);
// Register with AlbumViewPage for enlarge/shrink handling
AlbumViewPage albumPage = FindObjectOfType<AlbumViewPage>();
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.RegisterAlbumCard(albumCard);
@@ -208,7 +208,7 @@ namespace UI.CardSystem
}
// Register with AlbumViewPage for enlarge/shrink handling
AlbumViewPage albumPage = FindObjectOfType<AlbumViewPage>();
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.RegisterAlbumCard(albumCard);
@@ -284,7 +284,7 @@ namespace UI.CardSystem
previewCardDisplay.transform.localScale = _previewOriginalScale;
// Get AlbumViewPage to show backdrop and reparent
AlbumViewPage albumPage = FindObjectOfType<AlbumViewPage>();
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.ShowSlotPreview(this, previewCardDisplay.transform);
@@ -311,7 +311,7 @@ namespace UI.CardSystem
previewCardDisplay.SetPreviewMode(false, null);
// Get AlbumViewPage to hide backdrop
AlbumViewPage albumPage = FindObjectOfType<AlbumViewPage>();
AlbumViewPage albumPage = FindFirstObjectByType<AlbumViewPage>();
if (albumPage != null)
{
albumPage.HideSlotPreview(this, previewCardDisplay.transform, () =>

View File

@@ -41,7 +41,6 @@ namespace UI.CardSystem
// State
private bool _isFlipped = false;
private bool _isFlipping = false;
private bool _isHovering = false;
private TweenBase _idleHoverTween;
private CardData _cardData;
private Vector2 _originalPosition; // Track original spawn position
@@ -242,8 +241,6 @@ namespace UI.CardSystem
if (_isFlipped || _isFlipping)
return;
_isHovering = true;
// Scale up slightly on hover
Tween.LocalScale(transform, Vector3.one * hoverScaleMultiplier, 0.2f, 0f, Tween.EaseOutBack);
}
@@ -253,8 +250,6 @@ namespace UI.CardSystem
if (_isFlipped || _isFlipping)
return;
_isHovering = false;
// Scale back to normal
Tween.LocalScale(transform, Vector3.one, 0.2f, 0f, Tween.EaseOutBack);
}

View File

@@ -27,7 +27,6 @@ namespace UI.CardSystem
[Header("Animation Settings")]
[SerializeField] private float hoverAmount = 20f;
[SerializeField] private float hoverDuration = 1.5f;
[SerializeField] private float glowPulseMin = 0.9f;
[SerializeField] private float glowPulseMax = 1.1f;
[SerializeField] private float glowPulseDuration = 1.2f;

View File

@@ -39,15 +39,13 @@ namespace UI.Core
public override int ManagedAwakePriority => 50; // UI infrastructure
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
Logging.Debug("[UIPageController] Initialized");
}

View File

@@ -346,7 +346,7 @@ namespace UI.DragAndDrop.Core
protected virtual void FindAndSnapToSlot()
{
SlotContainer[] containers = FindObjectsOfType<SlotContainer>();
SlotContainer[] containers = FindObjectsByType<SlotContainer>(FindObjectsSortMode.None);
DraggableSlot closestSlot = null;
float closestDistance = float.MaxValue;

View File

@@ -56,11 +56,9 @@ namespace UI
// ManagedBehaviour configuration
public override int ManagedAwakePriority => 45; // UI infrastructure, before UIPageController
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Set up container reference early
@@ -74,7 +72,7 @@ namespace UI
}
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
Logging.Debug("[LoadingScreenController] Initialized");
}

View File

@@ -31,11 +31,9 @@ namespace UI
// After UIPageController (50)
public override int ManagedAwakePriority => 55;
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake(); // CRITICAL: Register with LifecycleManager!
// Set instance immediately so it's available before OnManagedAwake() is called
// Set instance immediately (early initialization)
_instance = this;
// Ensure we have a CanvasGroup for transitions
@@ -51,9 +49,9 @@ namespace UI
gameObject.SetActive(false);
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Subscribe to scene-dependent events - must be in OnManagedAwake, not OnSceneReady
// Subscribe to scene-dependent events - must be in OnManagedStart, not OnSceneReady
// because PauseMenu is in DontDestroyOnLoad and OnSceneReady only fires once
if (SceneManagerService.Instance != null)
{
@@ -73,7 +71,7 @@ namespace UI
Logging.Debug("[PauseMenu] Subscribed to SceneManagerService events");
}
protected override void OnSceneReady()
internal override void OnSceneReady()
{
// This only fires once for DontDestroyOnLoad objects, so we handle scene loads in OnManagedAwake
}

View File

@@ -115,14 +115,14 @@ namespace UI
private UIPageController _uiPageController;
private AppSwitcher _appSwitcherComponent;
private new void Awake()
internal override void OnManagedAwake()
{
base.Awake();
if (Instance != null)
{
Destroy(this);
return;
}
// Set instance immediately (early initialization)
_instance = this;
// Get UIPageController on same GameObject
@@ -135,7 +135,7 @@ namespace UI
InitializeReferences();
}
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Subscribe to UIPageController page changes for auto HUD management
if (_uiPageController != null)

View File

@@ -32,7 +32,7 @@ namespace UI.Tutorial
public override int ManagedAwakePriority => 200; // Tutorial runs late, after other systems
protected override void OnManagedAwake()
internal override void OnManagedStart()
{
// Ensure prompt is hidden initially (even before tutorial initialization)
if (tapPrompt != null)

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 53ec386bba82c1748886a5beb8468ecf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 15003, guid: 0000000000000000e000000000000000, type: 0}
m_Name: iOS
m_EditorClassIdentifier: UnityEditor.dll::UnityEditor.Build.Profile.BuildProfile
m_AssetVersion: 1
m_BuildTarget: 9
m_Subtarget: 0
m_PlatformId: ad48d16a66894befa4d8181998c3cb09
m_PlatformBuildProfile:
rid: 3475452038477774988
m_OverrideGlobalSceneList: 0
m_Scenes: []
m_ScriptingDefines: []
m_PlayerSettingsYaml:
m_Settings: []
references:
version: 2
RefIds:
- rid: 3475452038477774988
type: {class: iOSPlatformSettings, ns: UnityEditor.iOS, asm: UnityEditor.iOS.Extensions}
data:
m_Development: 0
m_ConnectProfiler: 0
m_BuildWithDeepProfilingSupport: 0
m_AllowDebugging: 0
m_WaitForManagedDebugger: 0
m_ManagedDebuggerFixedPort: 0
m_ExplicitNullChecks: 0
m_ExplicitDivideByZeroChecks: 0
m_ExplicitArrayBoundsChecks: 0
m_CompressionType: -1
m_InstallInBuildFolder: 0
m_InsightsSettingsContainer:
m_BuildProfileEngineDiagnosticsState: 2
m_iOSXcodeBuildConfig: 1
m_SymlinkSources: 0
m_PreferredXcode:
m_SymlinkTrampoline: 0

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 110a4eabb37dbaa428e55c751696cd1e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

120
docs/custom_log_console.md Normal file
View File

@@ -0,0 +1,120 @@
# Custom Log Console
## Overview
A centralized logging system with an advanced filtering console that automatically tags log entries with class and method names. Provides powerful filtering capabilities beyond Unity's default console, including multi-select filters, text search, time-range filtering, and log export.
## Using the Logging System in Code
All logging automatically captures the calling class and method name using `CallerMemberName` and `CallerFilePath` attributes. Simply call the static logging methods:
```csharp
using Core;
public class MyClass : ManagedBehaviour
{
internal override void OnManagedStart()
{
Logging.Debug("Initialization complete");
Logging.Info("Player spawned at position");
Logging.Warning("Missing configuration, using defaults");
Logging.Error("Failed to load required asset");
}
}
```
**Output format:**
```
[ClassName][MethodName] Your message
```
**Available methods:**
- `Logging.Debug(string message)` - Detailed diagnostic information
- `Logging.Info(string message)` - General informational messages
- `Logging.Warning(string message)` - Non-critical issues
- `Logging.Error(string message)` - Critical errors
**Note:** All logs are broadcast via the `Logging.OnLogEntryAdded` event and stored in a central buffer accessible via `Logging.GetRecentLogs()`.
---
## Opening the Console Window
**Menu:** `AppleHills > Custom Log Console`
You can open multiple independent console instances with different filter configurations to monitor separate systems simultaneously
---
## Console Interface
![Custom Log Console Interface](media/custom_console.png)
### Toolbar Controls
#### 🔵 Basic Controls (Blue outline)
- **Clear** - Clears all log entries and resets tag lists
- **Auto-scroll** - Automatically scrolls to newest entries when enabled
#### Filter Buttons (Persistent Popups)
All filter buttons open persistent popup windows that remain open during multi-selection. Changes apply when you click "Apply" or dismiss with "Close".
- **🔴 Classes Filter (Red outline)**
- Multi-select which classes to display
- Includes search box for quick filtering
- All/None quick actions
- **🟢 Methods Filter (Green outline)**
- Multi-select which methods to display
- Includes search box for quick filtering
- All/None quick actions
- **🟡 Levels Filter (Yellow outline)**
- Toggle Debug, Info, Warning, Error levels
- All/None quick actions
- **⏱ Time Filter**
- Opens utility window with MinMaxSlider
- Filter logs by timestamp range
- Enable/disable toggle with reset option
#### Search & Export
- **Search** - Full-text search across class names, method names, and message content
- **Export** - Save filtered logs to .txt file with timestamp
- **Count** - Shows `filtered/total` log count
---
## Visual Indicators
**Color Coding:**
- White: Debug/Info (normal operation)
- Yellow: Warning (non-critical issues)
- Red: Error (critical failures)
**Alternating Rows:** Light/dark grey backgrounds improve readability for dense log output.
---
## Technical Details
**Event Broadcasting:**
```csharp
Logging.OnLogEntryAdded += (LogEntry entry) => { /* handle */ };
```
**Manual Log Retrieval:**
```csharp
List<LogEntry> recentLogs = Logging.GetRecentLogs();
```
**LogEntry Structure:**
- `ClassName` - Captured from calling file path
- `MethodName` - Captured from `CallerMemberName`
- `Message` - User-provided message text
- `Level` - Debug/Info/Warning/Error enum
- `Timestamp` - Time.realtimeSinceStartup
- `FullFormattedMessage` - Complete formatted string
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB