cz.xprees.core 1.0.39 → 1.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,78 @@
1
+ using System;
2
+ using System.Collections.Concurrent;
3
+ using System.Reflection;
4
+ using UnityEditor;
5
+ using UnityEngine;
6
+
7
+ namespace Xprees.Core.Editor
8
+ {
9
+ /// Centralized Editor runner that coordinates Play Mode entry and exit transitions for stateful ScriptableObjects.
10
+ /// Resets objects marked with [ResetOnPlayMode] and restores mutated assets upon exiting Play Mode.
11
+ /// Can be migrated to future Unity lifecycle hooks in future versions.
12
+ [InitializeOnLoad]
13
+ public static class StatePlayModeLifecycleHook
14
+ {
15
+ private readonly static ConcurrentDictionary<Type, PlayModeResetTiming> timingCache = new();
16
+
17
+ static StatePlayModeLifecycleHook()
18
+ {
19
+ EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
20
+ EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
21
+ }
22
+
23
+ private static void OnPlayModeStateChanged(PlayModeStateChange change)
24
+ {
25
+ switch (change)
26
+ {
27
+ case PlayModeStateChange.EnteredPlayMode:
28
+ ResetPlayModeObjects(PlayModeResetTiming.EnterPlayMode);
29
+ break;
30
+
31
+ case PlayModeStateChange.ExitingPlayMode:
32
+ // Automatically revert all mutated ScriptableObjects to baseline before returning to Edit Mode
33
+ StateSnapshotService.RestoreAll();
34
+ ResetPlayModeObjects(PlayModeResetTiming.ExitPlayMode);
35
+ break;
36
+
37
+ case PlayModeStateChange.EnteredEditMode:
38
+ StateSnapshotService.ClearAll();
39
+ break;
40
+ }
41
+ }
42
+
43
+ /// Resets loaded ScriptableObjects decorated with [ResetOnPlayMode] matching the specified timing.
44
+ /// Respects StateLifetime (skips StateLifetime.Persistent).
45
+ public static void ResetPlayModeObjects(PlayModeResetTiming timing)
46
+ {
47
+ var loadedObjects = Resources.FindObjectsOfTypeAll<ScriptableObject>();
48
+ foreach (var so in loadedObjects)
49
+ {
50
+ if (!so || so.IsStateless() || so.GetStateLifetime() == StateLifetime.Persistent) continue;
51
+
52
+ var type = so.GetType();
53
+ var configuredTiming = GetPlayModeTiming(type);
54
+ if ((configuredTiming & timing) == 0) continue;
55
+
56
+ var resetMethod = type.GetMethod("ResetToDefault", BindingFlags.Public | BindingFlags.Instance);
57
+ if (resetMethod != null)
58
+ {
59
+ resetMethod.Invoke(so, null);
60
+ }
61
+ else if (so is IRuntimeStateOwner stateOwner)
62
+ {
63
+ stateOwner.ClearTransientState();
64
+ }
65
+ }
66
+ }
67
+
68
+ public static PlayModeResetTiming GetPlayModeTiming(Type type)
69
+ {
70
+ if (timingCache.TryGetValue(type, out var timing)) return timing;
71
+
72
+ var attr = type.GetCustomAttribute<ResetOnPlayModeAttribute>(true);
73
+ timing = attr?.Timing ?? PlayModeResetTiming.None;
74
+ timingCache[type] = timing;
75
+ return timing;
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: fc5682634fa047ffba504454cebd6951
3
+ timeCreated: 1789122780
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cz.xprees.core.editor",
3
- "rootNamespace": "Xprees.Core",
3
+ "rootNamespace": "Xprees.Core.Editor",
4
4
  "references": [
5
5
  "GUID:999c2ca78ab34358a1222750376a501c"
6
6
  ],
package/README.md CHANGED
@@ -1,15 +1,12 @@
1
- # Unity Core classes
1
+ # Unity Core classes - cz.xprees.core
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/cz.xprees.core)](https://www.npmjs.com/package/cz.xprees.core)
4
4
 
5
-
6
- This is base package containing a few common classes for all of my Unity projects.
5
+ Base package containing core systems, state lifecycle orchestration, and ScriptableObject utilities for Unity projects.
7
6
 
8
7
  ## Installation
9
8
 
10
- Install the package using npm scoped registry in `Project Settings > Package Manager > Scoped Registries`
11
-
12
- [Unity Docs - Install a UPM package from a Git URL](https://docs.unity3d.com/6000.1/Documentation/Manual/upm-ui-giturl.html)
9
+ Install the package using npm scoped registry in `Project Settings > Package Manager > Scoped Registries`:
13
10
 
14
11
  ```json
15
12
  {
@@ -19,7 +16,83 @@ Install the package using npm scoped registry in `Project Settings > Package Man
19
16
  "cz.xprees"
20
17
  ]
21
18
  }
19
+ ```
20
+
21
+ Then install `cz.xprees.core` via the Unity Package Manager.
22
+
23
+ ---
24
+
25
+ ## State Management & Lifecycle Architecture
26
+
27
+ The package provides automated, zero-boilerplate state snapshotting and restoration for ScriptableObjects across scenario boundaries and Editor play
28
+ mode transitions.
29
+
30
+ ### 1. State Lifetime (`StateLifetime`)
31
+
32
+ Every stateful ScriptableObject inherits from `DescriptionBaseSO` or declares its lifecycle scope via `[StatefulLifetime]`:
33
+
34
+ - **`StateLifetime.Scenario` (Default)**:
35
+ Scoped strictly to the active scenario. Captured before mutations and automatically restored to its baseline on scenario start/restart.
36
+ - **`StateLifetime.Session`**:
37
+ Persists across scenarios throughout a play session. Restored only on game/session boundaries or play mode exit.
38
+ - **`StateLifetime.Persistent`**:
39
+ Excluded from state snapshots. Ideal for immutable databases, user settings, or content saved externally to disk.
40
+
41
+ ```csharp
42
+ // Per-instance: Set the "Lifetime" dropdown in the Inspector.
43
+ // Per-class: Override via attribute:
44
+ [StatefulLifetime(StateLifetime.Persistent)]
45
+ public class GlobalPlayerConfigSO : DescriptionBaseSO { }
46
+ ```
47
+
48
+ ### 2. State Snapshot Service (`StateSnapshotService`)
49
+
50
+ A centralized, pure runtime service that handles:
51
+
52
+ - **`EnsureCaptured(ScriptableObject target)`**: Serializes the baseline JSON state prior to the first mutation.
53
+ - **`Restore(ScriptableObject target)`**: Reverts serialized fields in-place from baseline JSON and triggers
54
+ `IRuntimeStateOwner.ClearTransientState()`.
55
+ - **`RestoreAll(IEnumerable<ScriptableObject> targets, StateLifetime maxLifetime)`**: Multi-pass restoration that ensures all targets are
56
+ baseline-captured before restoring, preventing inter-object dependency races.
57
+ - **`[SnapshotIgnore]`**: Decorate fields to preserve their values across snapshot restorations.
22
58
 
59
+ ### 3. Editor PlayMode Lifecycle (`StatePlayModeLifecycleHook`)
60
+
61
+ Located in `cz.xprees.core.editor`, this centralized runner hooks into `EditorApplication.playModeStateChanged`:
62
+
63
+ - **`EnteredPlayMode`**: Resets stateful objects decorated with `[ResetOnPlayMode(PlayModeResetTiming.EnterPlayMode)]` (skips `Persistent` objects).
64
+ - **`ExitingPlayMode`**: Restores all mutated ScriptableObjects in-place back to their baseline via `StateSnapshotService.RestoreAll()` and clears
65
+ editor dirty flags before returning to edit mode.
66
+ - **`EnteredEditMode`**: Purges snapshot dictionaries.
67
+
68
+ ### 4. Play Mode Reset Attribute (`ResetOnPlayModeAttribute`)
69
+
70
+ Control Play Mode transition resets declaratively on classes:
71
+
72
+ ```csharp
73
+ [Flags]
74
+ public enum PlayModeResetTiming
75
+ {
76
+ None = 0,
77
+ EnterPlayMode = 1 << 0,
78
+ ExitPlayMode = 1 << 1,
79
+ Both = EnterPlayMode | ExitPlayMode
80
+ }
81
+
82
+ // Example: opt-out of play mode reset
83
+ [ResetOnPlayMode(PlayModeResetTiming.None)]
84
+ public class PersistentSettingsSO : ScriptableObject { }
85
+ ```
86
+
87
+ ### 5. Runtime State Ownership (`IRuntimeStateOwner`)
88
+
89
+ Implement `IRuntimeStateOwner` on objects with non-serialized runtime state (active handles, listeners, temporary collections):
90
+
91
+ ```csharp
92
+ public interface IRuntimeStateOwner
93
+ {
94
+ void ClearTransientState();
95
+ }
23
96
  ```
24
97
 
25
- Then simply install the package using the Unity Package Manager using the _NPM - xprees_ scope or by the package name `cz.xprees.core`.
98
+ Invoked automatically after serialized fields are restored by `StateSnapshotService`.
@@ -0,0 +1,33 @@
1
+ using System;
2
+
3
+ namespace Xprees.Core
4
+ {
5
+ [Flags]
6
+ public enum PlayModeResetTiming
7
+ {
8
+ /// No reset will occur on Play Mode transitions
9
+ None = 0,
10
+
11
+ /// Reset state when entering Play Mode
12
+ EnterPlayMode = 1 << 0,
13
+
14
+ /// Reset state when exiting Play Mode
15
+ ExitPlayMode = 1 << 1,
16
+
17
+ /// Reset state on both entering and exiting Play Mode
18
+ Both = EnterPlayMode | ExitPlayMode,
19
+ }
20
+
21
+ /// Controls whether and when a stateful ScriptableObject (such as a Variable) resets to its authored default value during Editor Play Mode transitions.
22
+ /// Hooked and executed centrally by StateSnapshotService.
23
+ [AttributeUsage(AttributeTargets.Class, Inherited = true)]
24
+ public sealed class ResetOnPlayModeAttribute : Attribute
25
+ {
26
+ public PlayModeResetTiming Timing { get; }
27
+
28
+ public ResetOnPlayModeAttribute(PlayModeResetTiming timing = PlayModeResetTiming.EnterPlayMode)
29
+ {
30
+ Timing = timing;
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 83876b95fdaf4120a761253103ef2bbc
3
+ timeCreated: 1789121968
@@ -1,6 +1,7 @@
1
1
  using System;
2
2
  using System.Collections.Concurrent;
3
3
  using System.Collections.Generic;
4
+ using System.Linq;
4
5
  using System.Reflection;
5
6
  using UnityEngine;
6
7
  using Object = UnityEngine.Object;
@@ -12,8 +13,9 @@ namespace Xprees.Core
12
13
  {
13
14
  // TODO later consider adding limits to not blow up memory - probably not an issue
14
15
  // TODO consider using OdinSerializer for more robust serialization in future
16
+
15
17
  /// Core engine service for zero-boilerplate capturing and in-place restoration
16
- /// of ScriptableObject serialized state between scenario runs.
18
+ /// of ScriptableObject serialized state across scenario and session boundaries.
17
19
  public static class StateSnapshotService
18
20
  {
19
21
  private readonly static Dictionary<int, string> snapshots = new();
@@ -99,6 +101,33 @@ namespace Xprees.Core
99
101
  }
100
102
  }
101
103
 
104
+ /// Restores a collection of ScriptableObjects to their baseline state up to the specified max lifetime.
105
+ /// Multi-pass restoration:
106
+ /// Pass 1 captures the baseline for any uncaptured objects before modifying state.
107
+ /// Pass 2 restores serialized fields and triggers IRuntimeStateOwner.ClearTransientState.
108
+ public static void RestoreAll(IEnumerable<ScriptableObject> targets, StateLifetime maxLifetime = StateLifetime.Scenario)
109
+ {
110
+ if (targets == null) return;
111
+
112
+ var targetsList = targets as ScriptableObject[] ?? targets.ToArray();
113
+
114
+ // Pass 1: Ensure baseline is captured for ALL objects before any object is restored
115
+ foreach (var so in targetsList)
116
+ {
117
+ if (!so || so.IsStateless() || so.GetStateLifetime() > maxLifetime) continue;
118
+
119
+ EnsureCaptured(so);
120
+ }
121
+
122
+ // Pass 2: Restore all objects to baseline (and clear transient state via Restore)
123
+ foreach (var so in targetsList)
124
+ {
125
+ if (!so || so.IsStateless() || so.GetStateLifetime() > maxLifetime) continue;
126
+
127
+ Restore(so);
128
+ }
129
+ }
130
+
102
131
  /// Restores all currently tracked ScriptableObjects to their baseline state.
103
132
  public static int RestoreAll()
104
133
  {
@@ -236,39 +265,6 @@ namespace Xprees.Core
236
265
  /// SubsystemRegistration executes before any Awake/OnEnable calls when entering Play Mode,
237
266
  /// ensuring static state dictionaries from previous play sessions or editor runs are completely purged.
238
267
  [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
239
- private static void ResetStatics() => ClearAll();
240
-
241
- #if UNITY_EDITOR
242
- [InitializeOnLoadMethod]
243
- private static void InitEditorLifecycle()
244
- {
245
- EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
246
- EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
247
- }
248
-
249
- private static void OnPlayModeStateChanged(PlayModeStateChange change)
250
- {
251
- // When exiting Play Mode, restore before scene objects begin tearing down
252
- if (change == PlayModeStateChange.ExitingPlayMode)
253
- {
254
- RestoreAll();
255
- return;
256
- }
257
-
258
- // Once fully transitioned back to Edit Mode, all scene unload and OnDisable/OnDestroy
259
- // lifecycle calls have finished. Re-run RestoreAll() to guarantee any mutations occurring
260
- // during teardown are cleanly reverted, then clear the snapshot tracking.
261
- if (change == PlayModeStateChange.EnteredEditMode)
262
- {
263
- var count = RestoreAll();
264
- if (count > 0)
265
- {
266
- Debug.Log($"[StateSnapshotService] Restored {count} stateful ScriptableObjects back to pre-play baseline.");
267
- }
268
-
269
- ClearAll();
270
- }
271
- }
272
- #endif
268
+ private static void ResetStaticFields() => ClearAll();
273
269
  }
274
270
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cz.xprees.core",
3
3
  "displayName": "Core Classes",
4
- "version": "1.0.39",
4
+ "version": "1.0.40",
5
5
  "unity": "2021.3",
6
6
  "description": "This package contains core classes and interfaces for Unity projects. It is intended to be used as a dependency for other packages. By providing base classes in a separate package, you can easily update them across all your projects.",
7
7
  "license": "Apache-2.0",