public new void AfterInitialize() { // Vanilla / new saves don't have the LevelSets list. if (LevelSets == null) { LevelSets = new List <LevelSetStats>(); } if (LevelSetRecycleBin == null) { LevelSetRecycleBin = new List <LevelSetStats>(); } if (LevelSets.Count <= 1 && LevelSetRecycleBin.Count == 0 && !HasModdedSaveData) { // the save file doesn't have any mod save data (just created, overwritten by vanilla, or Everest just updated). // we want to carry mod save data that was backed up in the mod save file, if any. ModSaveData modSaveData = UserIO.Load <ModSaveData>(GetFilename(FileSlot) + "-modsavedata"); if (modSaveData != null) { modSaveData.CopyToCelesteSaveData(this); Logger.Log(LogLevel.Warn, "SaveData", $"{LevelSets.Count} level set(s) were restored from mod backup for save slot {FileSlot}"); } } HasModdedSaveData = true; if (Areas_Unsafe == null) { Areas_Unsafe = new List <AreaStats>(); } // Add missing LevelSetStats. foreach (AreaData area in AreaData.Areas) { string set = area.GetLevelSet(); if (!LevelSets.Exists(other => other.Name == set)) { LevelSetStats recycleBinLevelSet = LevelSetRecycleBin.FirstOrDefault(other => other.Name == set); if (recycleBinLevelSet != null) { // the level set is actually in the recycle bin - restore it. LevelSets.Add(recycleBinLevelSet); LevelSetRecycleBin.Remove(recycleBinLevelSet); } else { // create a new LevelSetStats entry. LevelSets.Add(new LevelSetStats { Name = set, UnlockedAreas = set == "Celeste" ? UnlockedAreas_Unsafe : 0 }); } } } // Fill each LevelSetStats with its areas. for (int lsi = 0; lsi < LevelSets.Count; lsi++) { LevelSetStats set = LevelSets[lsi]; set.SaveData = this; List <AreaStats> areas = set.Areas; if (set.Name == "Celeste") { areas = Areas_Unsafe; } int offset = set.AreaOffset; if (offset == -1) { // LevelSet gone - let's move it to the recycle bin. LevelSetStats levelSetAlreadyInRecycleBin = LevelSetRecycleBin.FirstOrDefault(other => other.Name == set.Name); if (levelSetAlreadyInRecycleBin != null) { // a level set with the same name already exists in the recycle bin - replace it. LevelSetRecycleBin.Remove(levelSetAlreadyInRecycleBin); } LevelSetRecycleBin.Add(set); // now, remove it to prevent any unwanted access. LevelSets.RemoveAt(lsi); lsi--; continue; } // Refresh all stat IDs based on their SIDs, sort, fill and remove leftovers. // Temporarily use ID_Unsafe; later ID_Safe to ID_Unsafe to resync the SIDs. // This keeps the stats bound to their SIDs, not their indices, while removing non-existent areas. int countRoots = AreaData.Areas.Count(other => other.GetLevelSet() == set.Name && string.IsNullOrEmpty(other?.GetMeta()?.Parent)); int countAll = AreaData.Areas.Count(other => other.GetLevelSet() == set.Name); // Fix IDs for (int i = 0; i < areas.Count; i++) { AreaData area = AreaDataExt.Get(areas[i]); if (!string.IsNullOrEmpty(area?.GetMeta()?.Parent)) { area = null; } ((patch_AreaStats)areas[i]).ID_Unsafe = area?.ID ?? int.MaxValue; } // Sort areas.Sort((a, b) => ((patch_AreaStats)a).ID_Unsafe - ((patch_AreaStats)b).ID_Unsafe); // Remove leftovers while (areas.Count > 0 && ((patch_AreaStats)areas[areas.Count - 1]).ID_Unsafe == int.MaxValue) { areas.RemoveAt(areas.Count - 1); } // Fill gaps for (int i = 0; i < countRoots; i++) { if (i >= areas.Count || ((patch_AreaStats)areas[i]).ID_Unsafe != offset + i) { areas.Insert(i, new AreaStats(offset + i)); } } // Duplicate parent stat refs into their respective children slots. for (int i = countRoots; i < countAll; i++) { if (i >= areas.Count) { areas.Insert(i, areas[AreaDataExt.Get(AreaData.Get(offset + i).GetMeta().Parent).ID - offset]); } } // Resync SIDs for (int i = 0; i < areas.Count; i++) { ((patch_AreaStats)areas[i]).ID_Safe = ((patch_AreaStats)areas[i]).ID_Unsafe; } int lastCompleted = -1; for (int i = 0; i < countRoots; i++) { if (areas[i].Modes[0].Completed) { lastCompleted = i; } } if (set.Name == "Celeste") { if (UnlockedAreas_Unsafe < lastCompleted + 1 && set.MaxArea >= lastCompleted + 1) { UnlockedAreas_Unsafe = lastCompleted + 1; } if (DebugMode) { UnlockedAreas_Unsafe = set.MaxArea; } } else { if (set.UnlockedAreas < lastCompleted + 1 && set.MaxArea >= lastCompleted + 1) { set.UnlockedAreas = lastCompleted + 1; } if (DebugMode) { set.UnlockedAreas = set.MaxArea; } } foreach (AreaStats area in areas) { area.CleanCheckpoints(); } } // Assign SaveData for the level sets in the recycle bin to prevent crashes. foreach (LevelSetStats set in LevelSetRecycleBin) { set.SaveData = this; } // Order the levelsets to appear just as their areas appear in AreaData.Areas LevelSets.Sort((set1, set2) => set1.AreaOffset.CompareTo(set2.AreaOffset)); // If there is no mod progress, carry over any progress from vanilla saves. if (LastArea_Safe.ID == 0) { LastArea_Safe = LastArea_Unsafe; } if (CurrentSession_Safe == null) { CurrentSession_Safe = CurrentSession_Unsafe; } // Trick unmodded instances of Celeste to thinking that we last selected prologue / played no level. LastArea_Unsafe = AreaKey.Default; CurrentSession_Unsafe = null; // Fix areas with missing SID (f.e. deleted or renamed maps). if (AreaData.Get(LastArea) == null) { LastArea = AreaKey.Default; } // Fix out of bounds areas. if (LastArea.ID < 0 || LastArea.ID >= AreaData.Areas.Count) { LastArea = AreaKey.Default; } if (string.IsNullOrEmpty(TheoSisterName)) { TheoSisterName = Dialog.Clean("THEO_SISTER_NAME", null); if (Name.IndexOf(TheoSisterName, StringComparison.InvariantCultureIgnoreCase) >= 0) { TheoSisterName = Dialog.Clean("THEO_SISTER_ALT_NAME", null); } } AssistModeChecks(); if (Version != null) { Version v = new Version(Version); if (v < new Version(1, 2, 1, 1)) { for (int id = 0; id < Areas_Unsafe.Count; id++) { AreaStats area = Areas_Unsafe[id]; if (area == null) { continue; } for (int modei = 0; modei < area.Modes.Length; modei++) { AreaModeStats mode = area.Modes[modei]; if (mode == null) { continue; } if (mode.BestTime > 0L) { mode.SingleRunCompleted = true; } mode.BestTime = 0L; mode.BestFullClearTime = 0L; } } } } }
public new void AfterInitialize() { // Vanilla / new saves don't have the LevelSets list. if (LevelSets == null) { LevelSets = new List <LevelSetStats>(); } if (Areas_Unsafe == null) { Areas_Unsafe = new List <AreaStats>(); } // Add missing LevelSetStats. foreach (AreaData area in AreaData.Areas) { string set = area.GetLevelSet(); if (!LevelSets.Exists(other => other.Name == set)) { LevelSets.Add(new LevelSetStats { Name = set, UnlockedAreas = set == "Celeste" ? UnlockedAreas_Unsafe : 0 }); } } // Fill each LevelSetStats with its areas. for (int lsi = 0; lsi < LevelSets.Count; lsi++) { LevelSetStats set = LevelSets[lsi]; set.SaveData = this; List <AreaStats> areas = set.Areas; if (set.Name == "Celeste") { areas = Areas_Unsafe; } int offset = set.AreaOffset; if (offset == -1) { // LevelSet gone - let's remove it to prevent any unwanted accesses. // We previously kept the LevelSetStats around in case the levelset resurfaces later on, but as it turns out, this breaks some stuff. LevelSets.RemoveAt(lsi); lsi--; continue; } // Refresh all stat IDs based on their SIDs, sort, fill and remove leftovers. // Temporarily use ID_Unsafe; later ID_Safe to ID_Unsafe to resync the SIDs. // This keeps the stats bound to their SIDs, not their indices, while removing non-existent areas. int count = AreaData.Areas.Count(other => other.GetLevelSet() == set.Name); // Fix IDs for (int i = 0; i < areas.Count; i++) { ((patch_AreaStats)areas[i]).ID_Unsafe = AreaDataExt.Get(areas[i])?.ID ?? int.MaxValue; } // Sort areas.Sort((a, b) => ((patch_AreaStats)a).ID_Unsafe - ((patch_AreaStats)b).ID_Unsafe); // Remove leftovers while (areas.Count > 0 && ((patch_AreaStats)areas[areas.Count - 1]).ID_Unsafe == int.MaxValue) { areas.RemoveAt(areas.Count - 1); } // Fill gaps for (int i = 0; i < count; i++) { if (i >= areas.Count || ((patch_AreaStats)areas[i]).ID_Unsafe != offset + i) { areas.Insert(i, new AreaStats(offset + i)); } } // Resync SIDs for (int i = 0; i < areas.Count; i++) { ((patch_AreaStats)areas[i]).ID_Safe = ((patch_AreaStats)areas[i]).ID_Unsafe; } int lastCompleted = -1; for (int i = 0; i < count; i++) { if (areas[i].Modes[0].Completed) { lastCompleted = i; } } if (set.Name == "Celeste") { if (UnlockedAreas_Unsafe < lastCompleted + 1 && set.MaxArea >= lastCompleted + 1) { UnlockedAreas_Unsafe = lastCompleted + 1; } if (DebugMode) { UnlockedAreas_Unsafe = set.MaxArea; } } else { if (set.UnlockedAreas < lastCompleted + 1 && set.MaxArea >= lastCompleted + 1) { set.UnlockedAreas = lastCompleted + 1; } if (DebugMode) { set.UnlockedAreas = set.MaxArea; } } foreach (AreaStats area in areas) { area.CleanCheckpoints(); } } // Order the levelsets to appear just as their areas appear in AreaData.Areas LevelSets.OrderBy(set => set.AreaOffset); // Carry over any progress from vanilla saves. if (LastArea_Unsafe.ID != 0) { LastArea_Safe = LastArea_Unsafe; } if (CurrentSession_Unsafe != null) { CurrentSession_Safe = CurrentSession_Unsafe; } // Trick unmodded instances of Celeste to thinking that we last selected prologue / played no level. LastArea_Unsafe = AreaKey.Default; CurrentSession_Unsafe = null; // Fix out of bounds areas. if (LastArea.ID < 0 || LastArea.ID >= AreaData.Areas.Count) { LastArea = AreaKey.Default; } // Debug mode shouldn't auto-enter into a level. if (DebugMode) { CurrentSession = null; } if (string.IsNullOrEmpty(TheoSisterName)) { TheoSisterName = Dialog.Clean("THEO_SISTER_NAME", null); if (Name.IndexOf(TheoSisterName, StringComparison.InvariantCultureIgnoreCase) >= 0) { TheoSisterName = Dialog.Clean("THEO_SISTER_ALT_NAME", null); } } if (!AssistMode) { Assists = default(Assists); } // Note to future person trying to disable this clamping: // Audio snapshots matching the speed are being used. // Removing this clamp kills the game. if (Assists.GameSpeed == 0) { Assists.GameSpeed = 10; } if (Assists.GameSpeed < 5 || Assists.GameSpeed > 10) { Assists.GameSpeed = 10; } Everest.Invoke("LoadSaveData", FileSlot); }