-
Notifications
You must be signed in to change notification settings - Fork 0
/
SGBSaveBridgeService.cs
200 lines (170 loc) · 8.01 KB
/
SGBSaveBridgeService.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
using Naninovel;
using System.IO;
namespace BobboNet.SGB.IMod.Naninovel
{
/// <summary>
/// This service connects Naninovel's audio with SGB's audio using IMod.
/// </summary>
[InitializeAtRuntime]
public class SGBSaveBridgeService : IEngineService
{
//
// Classes
//
[System.Serializable]
private class SGBSaveState
{
public int saveIndex;
public string serializedState;
public System.DateTime saveTime;
}
//
// Variables
//
private IStateManager stateManager; // Naninovel's game state manager
private SGBSaveState currentSaveState = null; // The last SGB save state
//
// Constructors
//
public SGBSaveBridgeService(IStateManager stateManager)
{
this.stateManager = stateManager;
}
//
// Interface Methods
//
public UniTask InitializeServiceAsync()
{
// Initialize the service here.
var configIMod = Engine.GetConfiguration<SGBIModConfiguration>();
// Connect to Naninovel
stateManager.AddOnGameSerializeTask(OnNaninovelStateSerialize);
stateManager.AddOnGameDeserializeTask(OnNaninovelDeserializeState);
// Connect to SGB
SGBSaveManager.SaveDataOverrideFunc = OnSGBSave;
SGBSaveManager.ReadSaveInfoOverrideFunc = OnSGBReadSaveInfo;
SGBSaveManager.LoadDataOverrideFunc = OnSGBLoad;
SGBSaveManager.MaxSaveFileCount = configIMod.maxSGBSaveSlots;
// Apply our config to SGB's pause menu options
SGBPauseMenuOptions.ItemsButton.IsVisible = configIMod.itemsButton.isVisible;
SGBPauseMenuOptions.ItemsButton.IsInteractable = configIMod.itemsButton.isInteractable;
SGBPauseMenuOptions.SkillsButton.IsVisible = configIMod.skillsButton.isVisible;
SGBPauseMenuOptions.SkillsButton.IsInteractable = configIMod.skillsButton.isInteractable;
SGBPauseMenuOptions.EquipmentButton.IsVisible = configIMod.equipmentButton.isVisible;
SGBPauseMenuOptions.EquipmentButton.IsInteractable = configIMod.equipmentButton.isInteractable;
SGBPauseMenuOptions.StatusButton.IsVisible = configIMod.statusButton.isVisible;
SGBPauseMenuOptions.StatusButton.IsInteractable = configIMod.statusButton.isInteractable;
SGBPauseMenuOptions.SaveButton.IsVisible = configIMod.saveButton.isVisible;
SGBPauseMenuOptions.SaveButton.IsInteractable = configIMod.saveButton.isInteractable;
SGBPauseMenuOptions.ConfigButton.IsVisible = configIMod.configButton.isVisible;
SGBPauseMenuOptions.ConfigButton.IsInteractable = configIMod.configButton.isInteractable;
SGBPauseMenuOptions.CloseButton.IsVisible = configIMod.closeButton.isVisible;
SGBPauseMenuOptions.CloseButton.IsInteractable = configIMod.closeButton.isInteractable;
SGBPauseMenuOptions.ExitButton.IsVisible = configIMod.exitButton.isVisible;
SGBPauseMenuOptions.ExitButton.IsInteractable = configIMod.exitButton.isInteractable;
return UniTask.CompletedTask;
}
public void ResetService()
{
// Reset service state here.
currentSaveState = null;
}
public void DestroyService()
{
// Stop the service and release any used resources here.
// Disconnect from Naninovel
stateManager.RemoveOnGameSerializeTask(OnNaninovelStateSerialize);
stateManager.RemoveOnGameDeserializeTask(OnNaninovelDeserializeState);
// Disconnect from SGB
SGBSaveManager.SaveDataOverrideFunc = null;
SGBSaveManager.ReadSaveInfoOverrideFunc = null;
SGBSaveManager.LoadDataOverrideFunc = null;
SGBPauseMenuOptions.SaveButton.IsVisible = true;
SGBPauseMenuOptions.SaveButton.IsInteractable = true;
}
//
// Private Methods
//
/// <summary>
/// A callback that's called when Naninovel serializes the current game state (for saving)
/// </summary>
/// <param name="stateMap">The object to serialize the current game state into.</param>
private void OnNaninovelStateSerialize(GameStateMap stateMap)
{
if (currentSaveState == null) return;
stateMap.SetState(currentSaveState);
}
/// <summary>
/// A callback that's called when Naninovel deserializes a game state (when loading)
/// </summary>
/// <param name="stateMap"></param>
/// <returns></returns>
private UniTask OnNaninovelDeserializeState(GameStateMap stateMap)
{
// Try to load the SGB save
var foundSave = stateMap.GetState<SGBSaveState>();
if (foundSave == null) return UniTask.CompletedTask;
// ...and if there is one, cache it
currentSaveState = foundSave;
return UniTask.CompletedTask;
}
/// <summary>
/// A callback that's called when SGB serializes it's game state, and wants to write it somewhere.
/// </summary>
/// <param name="saveIndex">Which SGB save-data is being saved</param>
/// <param name="dataToSave">The raw binary data to be saved.</param>
private void OnSGBSave(int saveIndex, Stream dataToSave)
{
SGBSaveState newSave = new SGBSaveState
{
saveIndex = saveIndex,
saveTime = System.DateTime.Now
};
// Rewind the data to the beginning & convert it to a string we can handle
using (var conversionBuffer = new MemoryStream())
{
dataToSave.Seek(0, SeekOrigin.Begin);
dataToSave.CopyTo(conversionBuffer);
newSave.serializedState = System.Convert.ToBase64String(conversionBuffer.ToArray());
}
// If all is well, update our cached state
currentSaveState = newSave;
}
/// <summary>
/// A callback that's called when SGB wants to know more about a specific save file
/// </summary>
/// <param name="saveIndex">The index of the save file to query.</param>
/// <returns>A class describing information about the given save slot.</returns>
private SGBSaveInfo OnSGBReadSaveInfo(int saveIndex)
{
// If we don't have the requested save file, EXIT EARLY.
if (!HasSaveAtIndex(saveIndex)) return SGBSaveInfo.NewEmpty(saveIndex);
// OTHERWISE, we DO have this save file, so return relevant info
return SGBSaveInfo.NewReal(saveIndex, currentSaveState.saveTime);
}
/// <summary>
/// A callback that's called when SGB wants to deserialize it's game state, and needs to read it from somewhere.
/// </summary>
/// <param name="saveIndex">Which SGB save-data is being loaded</param>
/// <returns>A stream containing the loaded data</returns>
private Stream OnSGBLoad(int saveIndex)
{
// If we don't have a save at the given index, EXIT EARLY
if (!HasSaveAtIndex(saveIndex)) return null;
// OTHERWISE - convert our base64 string into some data
return new MemoryStream(System.Convert.FromBase64String(currentSaveState.serializedState));
}
/// <summary>
/// Do we have an SGB save file stored with the given index?
/// </summary>
/// <param name="saveIndex">The index of the SGB save to look for.</param>
/// <returns>true if we have that save, false otherwise.</returns>
private bool HasSaveAtIndex(int saveIndex)
{
if (currentSaveState == null) return false;
if (currentSaveState.saveIndex != saveIndex) return false;
if (string.IsNullOrEmpty(currentSaveState.serializedState)) return false;
return true;
}
}
}