Skip to content

Commit 8a35aa8

Browse files
savemanager and guidmanager
1 parent e8dd50d commit 8a35aa8

5 files changed

Lines changed: 290 additions & 10 deletions

File tree

InscryptionAPI/Challenges/ChallengeManager.cs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using UnityEngine;
88
using System.Diagnostics.CodeAnalysis;
99
using System.Collections.Generic;
10+
using InscryptionAPI.Guid;
1011

1112
namespace InscryptionAPI.Challenges
1213
{
@@ -15,8 +16,6 @@ public static class ChallengeManager
1516
{
1617
public static readonly ReadOnlyCollection<AscensionChallengeInfo> BaseGameChallenges = new(Resources.LoadAll<AscensionChallengeInfo>("Data/"));
1718

18-
private static AscensionChallenge lastCustomChallenge = (AscensionChallenge)500;
19-
2019
private static Dictionary<AscensionChallenge, bool> stackableMap = new();
2120

2221
private static Dictionary<AscensionChallenge, int> unlockLevelMap = new();
@@ -44,23 +43,22 @@ public static bool IsStackable(AscensionChallenge id)
4443
return stackableMap.ContainsKey(id) ? stackableMap[id] : false;
4544
}
4645

47-
public static AscensionChallenge Add(AscensionChallengeInfo info, int unlockLevel=0, bool stackable=false)
46+
public static AscensionChallenge Add(string pluginGuid, AscensionChallengeInfo info, int unlockLevel=0, bool stackable=false)
4847
{
49-
AscensionChallenge newId = (AscensionChallenge)((int)lastCustomChallenge + 1);
50-
lastCustomChallenge = newId;
51-
info.challengeType = newId;
48+
info.challengeType = (AscensionChallenge)GuidManager.GetEnumValue<AscensionChallenge>(pluginGuid, info.title);
5249

5350
newInfos.Add(info);
5451

5552
ModifyAscensionChallengeList?.Invoke(newInfos);
5653

57-
stackableMap.Add(newId, stackable);
58-
unlockLevelMap.Add(newId, unlockLevel);
54+
stackableMap.Add(info.challengeType, stackable);
55+
unlockLevelMap.Add(info.challengeType, unlockLevel);
5956

60-
return newId;
57+
return info.challengeType;
6158
}
6259

6360
public static AscensionChallenge Add(
61+
string pluginGuid,
6462
string title,
6563
string description,
6664
int pointValue,
@@ -80,7 +78,7 @@ public static AscensionChallenge Add(
8078
Texture2D infoActivationTexture = activatedTexture ?? Resources.Load<Texture2D>("art/ui/ascension/ascensionicon_activated_default");
8179
info.activatedSprite = Sprite.Create(infoActivationTexture, SPRITE_RECT, SPRITE_PIVOT);
8280

83-
return Add(info, unlockLevel, stackable);
81+
return Add(pluginGuid, info, unlockLevel, stackable);
8482
}
8583

8684
[HarmonyPatch(typeof(AscensionChallengesUtil), "GetInfo")]

InscryptionAPI/Guid/GuidManager.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using DiskCardGame;
2+
using System;
3+
using System.Linq;
4+
using HarmonyLib;
5+
using Mono.Collections.Generic;
6+
using Sirenix.Serialization.Utilities;
7+
using UnityEngine;
8+
using System.Diagnostics.CodeAnalysis;
9+
using System.Collections.Generic;
10+
using System.Reflection;
11+
using System.IO;
12+
using APIPlugin;
13+
using InscryptionAPI.Saves;
14+
15+
namespace InscryptionAPI.Guid
16+
{
17+
public static class GuidManager
18+
{
19+
public static string GetFullyQualifiedName(string guid, string value)
20+
{
21+
return $"{guid}_{value}";
22+
}
23+
24+
public static readonly int START_INDEX = 500;
25+
26+
public const string MAX_DATA = "maximumStoredValueForEnum";
27+
28+
public static int GetEnumValue<T>(string guid, string value) where T : System.Enum
29+
{
30+
string saveKey = GetFullyQualifiedName(guid, value);
31+
string saveGuid = GetFullyQualifiedName("cyantist.inscryption.api", typeof(T).Name);
32+
33+
int enumValue = ModdedSaveManager.SaveData.GetValueAsInt(saveGuid, saveKey);
34+
35+
if (enumValue > 0)
36+
return enumValue;
37+
38+
enumValue = ModdedSaveManager.SaveData.GetValueAsInt(saveGuid, MAX_DATA) + 1;
39+
if (enumValue < START_INDEX)
40+
enumValue = START_INDEX;
41+
42+
ModdedSaveManager.SaveData.SetValue(saveGuid, MAX_DATA, enumValue);
43+
ModdedSaveManager.SaveData.SetValue(saveGuid, saveKey, enumValue);
44+
45+
ModdedSaveManager.isSystemDirty = true;
46+
47+
return enumValue;
48+
}
49+
}
50+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using DiskCardGame;
2+
using System;
3+
using System.Linq;
4+
using HarmonyLib;
5+
using Mono.Collections.Generic;
6+
using Sirenix.Serialization.Utilities;
7+
using UnityEngine;
8+
using System.Diagnostics.CodeAnalysis;
9+
using System.Collections.Generic;
10+
using System.Reflection;
11+
using System.IO;
12+
13+
namespace InscryptionAPI.Saves
14+
{
15+
public class ModdedSaveData
16+
{
17+
internal Dictionary<string, Dictionary<string, string>> SaveData = new();
18+
19+
public string GetValue(string guid, string key)
20+
{
21+
if (!SaveData.ContainsKey(guid))
22+
SaveData.Add(guid, new());
23+
24+
if (!SaveData[guid].ContainsKey(key))
25+
SaveData[guid].Add(key, default(string));
26+
27+
return SaveData[guid][key];
28+
}
29+
30+
public int GetValueAsInt(string guid, string key)
31+
{
32+
string value = GetValue(guid, key);
33+
int result;
34+
int.TryParse(value, out result);
35+
return result;
36+
}
37+
38+
public float GetValueAsFloat(string guid, string key)
39+
{
40+
string value = GetValue(guid, key);
41+
float result;
42+
float.TryParse(value, out result);
43+
return result;
44+
}
45+
46+
public bool GetValueAsBoolean(string guid, string key)
47+
{
48+
string value = GetValue(guid, key);
49+
bool result;
50+
bool.TryParse(value, out result);
51+
return result;
52+
}
53+
54+
public void SetValue(string guid, string key, object value)
55+
{
56+
if (!SaveData.ContainsKey(guid))
57+
SaveData.Add(guid, new());
58+
59+
if (!SaveData[guid].ContainsKey(key))
60+
SaveData[guid].Add(key, value.ToString());
61+
else
62+
SaveData[guid][key] = value.ToString();
63+
}
64+
}
65+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using DiskCardGame;
2+
using System;
3+
using System.Linq;
4+
using HarmonyLib;
5+
using Mono.Collections.Generic;
6+
using Sirenix.Serialization.Utilities;
7+
using UnityEngine;
8+
using System.Diagnostics.CodeAnalysis;
9+
using System.Collections.Generic;
10+
using System.Reflection;
11+
using System.IO;
12+
using APIPlugin;
13+
14+
namespace InscryptionAPI.Saves
15+
{
16+
[HarmonyPatch]
17+
public static class ModdedSaveManager
18+
{
19+
private static readonly string saveFilePath = Path.Combine(Assembly.GetExecutingAssembly().Location, "..", "..", "ModdedSaveFile.gwsave");
20+
21+
public static ModdedSaveData SaveData { get; private set; }
22+
23+
public static ModdedSaveData RunState { get; private set; }
24+
25+
internal static bool isSystemDirty = false; // This set whenever we save important system data
26+
// This only happens during initialization
27+
// We use it to make sure that loading data doesn't overwrite system data.
28+
29+
static ModdedSaveManager()
30+
{
31+
ReadDataFromFile();
32+
}
33+
34+
[HarmonyPatch(typeof(SaveManager), "SaveToFile")]
35+
[HarmonyPostfix]
36+
public static void SaveDataToFile()
37+
{
38+
Tuple<Dictionary<string, Dictionary<string, string>>, Dictionary<string, Dictionary<string, string>>> saveData = new(SaveData.SaveData, RunState.SaveData);
39+
string moddedSaveData = SaveManager.ToJSON(saveData);
40+
File.WriteAllText(saveFilePath, moddedSaveData);
41+
}
42+
43+
[HarmonyPatch(typeof(SaveManager), "LoadFromFile")]
44+
[HarmonyPostfix]
45+
public static void ReadDataFromFile()
46+
{
47+
if (isSystemDirty)
48+
SaveDataToFile();
49+
50+
if (File.Exists(saveFilePath))
51+
{
52+
string json = File.ReadAllText(saveFilePath);
53+
Plugin.Log.LogInfo($"Save data {json}");
54+
Tuple<Dictionary<string, Dictionary<string, string>>, Dictionary<string, Dictionary<string, string>>> saveData;
55+
saveData = SaveManager.FromJSON<Tuple<Dictionary<string, Dictionary<string, string>>, Dictionary<string, Dictionary<string, string>>>>(json);
56+
Plugin.Log.LogInfo($"Save data {saveData}");
57+
58+
if (SaveData == null)
59+
SaveData = new();
60+
61+
if (RunState == null)
62+
RunState = new();
63+
64+
SaveData.SaveData = saveData.Item1;
65+
RunState.SaveData = saveData.Item2;
66+
}
67+
else
68+
{
69+
SaveData = new();
70+
RunState = new();
71+
}
72+
}
73+
74+
[HarmonyPatch(typeof(AscensionSaveData), "NewRun")]
75+
[HarmonyPostfix]
76+
public static void ResetRunStateOnNewAscensionRun()
77+
{
78+
RunState = new();
79+
}
80+
81+
[HarmonyPatch(typeof(SaveFile), "NewPart1Run")]
82+
[HarmonyPrefix]
83+
public static void ResetRunStateOnPart1Run()
84+
{
85+
RunState = new();
86+
}
87+
}
88+
}

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,85 @@ in **Inscryption/BepInEx/Config/BepInEx/cfg**
4848
___
4949
If you want help debugging you can find me on the [Inscryption Modding Discord](https://discord.gg/QrJEF5Denm) or on [Daniel Mullins Discord](https://discord.com/invite/danielmullinsgames) as Cyantist.
5050

51+
## Using the API
52+
53+
### Custom Save Game Data
54+
If your mod needs to save data, the ModdedSaveManager class is here to help. There are two chunks of extra save data that you can access here: 'SaveData' (which persists across runs) and 'RunState' (which is reset on every run). Note that these require you to pass in a GUID, which should be your mod's plugin GUID, and an arbitrary key, which you can select for each property to you want to save.
55+
56+
The easiest way to use these helpers is to map them behind static properties, like so:
57+
58+
```
59+
public static int NumberOfItems
60+
{
61+
get { return ModdedSaveManager.SaveData.GetValueAsInt(Plugin.PluginGuid, "NumberOfItems"); }
62+
set { ModdedSaveManager.SaveData.SetValue(Plugin.PluginGuid, "NumberOfItems", value); }
63+
}
64+
```
65+
66+
When written like this, the static property "NumberOfItems" now automatically syncs to the save file.
67+
68+
### Adding new Challenges
69+
The API supports adding new Challenges as part of Kaycee's Mod using the ChallengeManager class. You can add a new Challenge using ChallengeManager.Add, either by passing in a new AscensionChallengeInfo object or by passing the individual properties of your challenge (which will construct the information object for you). This will make your challenge automatically appear in the challenge selection screen.
70+
71+
If you use the overload of Add that takes an AscensionChallengeInfo object, note that the "challengeType" field of type AscensionChallenge is completely irrelevant. This is an enumerated value, and it will be set for you by the ChallengeManager to ensure there is no collision with other challenges created by other mods. As such, you need to save the ID that is returned by the Add method of ChallengeManager so that you can reference it later.
72+
73+
If your challenge can stack, set the stackable flag to 'true' when adding your challenge. This will cause it to appear twice in the challenge selection screen. Yes, this means stackable challenges are currently limited to a max of two.
74+
75+
You will be responsible to write all of the necessary patches for your challenge to function. When writing those patches, you *must* make sure that the challenge is active before you do anything. This is entirely up to you - there is nothing in the API that can detect this for you.
76+
77+
You should also make sure to alert the user whenever your challenge has triggered some change in the game. The ChallengeActivationUI class (from DiskCardGame) has a helper to alert the user.
78+
79+
```
80+
private static AscensionChallenge ID = ChallengeManager.Add
81+
(
82+
Plugin.PluginGuid,
83+
"Dummy Challenge",
84+
"This challenge doesn't do anything",
85+
15,
86+
Resources.Load<Texture2D>("Path/To/Texture")
87+
);
88+
89+
[HarmonyPatch(...)]
90+
public static void DoSomething()
91+
{
92+
if (AscensionSaveData.Data.ChallengeIsActive(ID))
93+
{
94+
ChallengeActivationUI.Instance.ShowActivation(ID);
95+
// Do some actual stuff
96+
}
97+
}
98+
```
99+
100+
### Adding Custom Screens to Kaycee's Mod
101+
102+
This API supports adding new screens to Kaycee's Mod that execute before a run starts. New screens can use the AscensionScreenSort attribute to influence their sort order. Custom screens will execute in the following order:
103+
104+
1. Starter Decks
105+
2. Select Challenges
106+
3. Custom Screens
107+
1. Requires Start
108+
2. Prefers Start
109+
3. No Preference (default)
110+
4. Prefers End
111+
5. Requires End
112+
4. Start Run
113+
114+
To create a custom screen, you need to write a special screen behavior class that inherits from AscensionRunSetupScreenBase (which in turn inherits from MonoBehavior). You will be required to override the following:
115+
116+
- headerText: The displayed title on the screen
117+
- showCardDisplayer: Set this to return true if you want the panel on your screen that shows card information
118+
- showCardPanel: Set this to return true if you want the scrollable panel on your screen that shows selectable cards
119+
120+
There is also a virtual method called InitializeScreen which you should use to build the content of your screen.
121+
122+
In general, you are responsible for doing all the hard work of building your screen. However, all the boilerplate content is built for you. You will automatically be given the continue and back buttons, the header (which shows the current challenge level), the footer (which displays changes as the challenge level changes), and the title of your screen. You can also optionally be given a scrollable card selection panel and card information displayer panel if you need them (using the properties shown above).
123+
124+
Once you've written the custom screen class, you need to register it with AscensionScreenManager like so:
125+
126+
```
127+
AscensionScreenManager.RegisterScreen<MyCustomScreen>();
128+
```
129+
51130
## Development
52131
At the moment I am working on:
53132

0 commit comments

Comments
 (0)