A game about forced loneliness, made by TACStudios
1using System;
2using System.Linq;
3using UnityEngine;
4using UnityObject = UnityEngine.Object;
5
6namespace Unity.VisualScripting
7{
8 public static class SavedVariables
9 {
10 #region Storage
11
12 public const string assetPath = "SavedVariables";
13
14 public const string playerPrefsKey = "LudiqSavedVariables";
15
16 private static VariablesAsset _asset;
17
18 public static VariablesAsset asset
19 {
20 get
21 {
22 if (_asset == null)
23 {
24 Load();
25 }
26
27 return _asset;
28 }
29 }
30
31 public static void Load()
32 {
33 _asset = Resources.Load<VariablesAsset>(assetPath) ?? ScriptableObject.CreateInstance<VariablesAsset>();
34 }
35
36 #endregion
37
38 #region Lifecycle
39
40 public static void OnEnterEditMode()
41 {
42 FetchSavedDeclarations();
43 DestroyMergedDeclarations(); // Required because assemblies don't reload on play mode exit
44 }
45
46 public static void OnExitEditMode()
47 {
48 SaveDeclarations(saved);
49 }
50
51 internal static void OnEnterPlayMode()
52 {
53 FetchSavedDeclarations();
54 MergeInitialAndSavedDeclarations();
55
56 // The variables saver gameobject is only instantiated if its needed
57 // It's only needed if a variable in our merged collection changes, requiring re-serialization as
58 // the runtime ends
59 merged.OnVariableChanged += () =>
60 {
61 if (VariablesSaver.instance == null)
62 VariablesSaver.Instantiate();
63 };
64 }
65
66 internal static void OnExitPlayMode()
67 {
68 SaveDeclarations(merged);
69 }
70
71 #endregion
72
73 #region Declarations
74
75 public static VariableDeclarations initial => asset.declarations;
76
77 public static VariableDeclarations saved { get; private set; }
78
79 public static VariableDeclarations merged { get; private set; }
80
81 public static VariableDeclarations current => Application.isPlaying ? merged : initial;
82
83 public static void SaveDeclarations(VariableDeclarations declarations)
84 {
85 WarnAndNullifyUnityObjectReferences(declarations);
86
87 try
88 {
89 var data = declarations.Serialize();
90
91 if (data.objectReferences.Length != 0)
92 {
93 // Hopefully, WarnAndNullify will have prevented this exception,
94 // but in case an object reference was nested as a member of the
95 // serialized objects, it wouldn't have caught it, and thus we need
96 // to abort the save process and inform the user.
97 throw new InvalidOperationException("Cannot use Unity object variable references in saved variables.");
98 }
99
100 PlayerPrefs.SetString(playerPrefsKey, data.json);
101 PlayerPrefs.Save();
102 }
103 catch (Exception ex)
104 {
105 Debug.LogWarning($"Failed to save variables to player prefs: \n{ex}");
106 }
107 }
108
109 public static void FetchSavedDeclarations()
110 {
111 if (PlayerPrefs.HasKey(playerPrefsKey))
112 {
113 try
114 {
115 saved = (VariableDeclarations)new SerializationData(PlayerPrefs.GetString(playerPrefsKey)).Deserialize();
116 }
117 catch (Exception ex)
118 {
119 Debug.LogWarning($"Failed to fetch saved variables from player prefs: \n{ex}");
120 saved = new VariableDeclarations();
121 }
122 }
123 else
124 {
125 saved = new VariableDeclarations();
126 }
127 }
128
129 private static void MergeInitialAndSavedDeclarations()
130 {
131 merged = initial.CloneViaFakeSerialization();
132
133 WarnAndNullifyUnityObjectReferences(merged);
134
135 foreach (var name in saved.Select(vd => vd.name))
136 {
137 if (!merged.IsDefined(name))
138 {
139 merged[name] = saved[name];
140 }
141 else if (merged[name] == null)
142 {
143 if (saved[name] == null || saved[name].GetType().IsNullable())
144 {
145 merged[name] = saved[name];
146 }
147 else
148 {
149 Debug.LogWarning($"Cannot convert saved player pref '{name}' to null.\n");
150 }
151 }
152 else
153 {
154 if (saved[name].IsConvertibleTo(merged[name].GetType(), true))
155 {
156 merged[name] = saved[name];
157 }
158 else
159 {
160 Debug.LogWarning($"Cannot convert saved player pref '{name}' to expected type ({merged[name].GetType()}).\nReverting to initial value.");
161 }
162 }
163 }
164 }
165
166 private static void DestroyMergedDeclarations()
167 {
168 merged = null;
169 }
170
171 private static void WarnAndNullifyUnityObjectReferences(VariableDeclarations declarations)
172 {
173 Ensure.That(nameof(declarations)).IsNotNull(declarations);
174
175 foreach (var declaration in declarations)
176 {
177 if (declaration.value is UnityObject)
178 {
179 Debug.LogWarning($"Saved variable '{declaration.name}' refers to a Unity object. This is not supported. Its value will be null.");
180 declarations[declaration.name] = null;
181 }
182 }
183 }
184
185 #endregion
186 }
187}