public new void CleanCheckpoints() { AreaData area = AreaData.Get(ID); for (int i = 0; i < Modes.Length; i++) { AreaMode areaMode = (AreaMode)i; AreaModeStats areaModeStats = Modes[i]; ModeProperties modeProperties = null; if (area.HasMode(areaMode)) { modeProperties = area.Mode[i]; } HashSet <string> checkpoints = new HashSet <string>(areaModeStats.Checkpoints); areaModeStats.Checkpoints.Clear(); if (modeProperties != null && modeProperties.Checkpoints != null) { foreach (CheckpointData checkpointData in modeProperties.Checkpoints) { if (checkpoints.Contains(checkpointData.Level)) { areaModeStats.Checkpoints.Add(checkpointData.Level); } } } } }
private static void OnChapterPanelUpdateStats(On.Celeste.OuiChapterPanel.orig_UpdateStats orig, OuiChapterPanel self, bool wiggle, bool?overrideStrawberryWiggle, bool?overrideDeathWiggle, bool?overrideHeartWiggle) { orig(self, wiggle, overrideStrawberryWiggle, overrideDeathWiggle, overrideHeartWiggle); if (Engine.Scene == overworldWrapper?.Scene) { AreaModeStats areaModeStats = self.DisplayedStats.Modes[(int)self.Area.Mode]; DeathsCounter deathsCounter = new DynData <OuiChapterPanel>(self).Get <DeathsCounter>("deaths"); deathsCounter.Visible = areaModeStats.Deaths > 0 && !AreaData.Get(self.Area).Interlude_Safe; // mod the death icon string pathToSkull = "CollabUtils2/skulls/" + self.Area.GetLevelSet(); if (GFX.Gui.Has(pathToSkull)) { new DynData <DeathsCounter>(deathsCounter)["icon"] = GFX.Gui[pathToSkull]; } } if (isPanelShowingLobby(self) || Engine.Scene == overworldWrapper?.Scene) { // turn strawberry counter into golden if there is no berry in the map if (AreaData.Get(self.Area).Mode[0].TotalStrawberries == 0) { StrawberriesCounter strawberriesCounter = new DynData <OuiChapterPanel>(self).Get <StrawberriesCounter>("strawberries"); strawberriesCounter.Golden = true; strawberriesCounter.ShowOutOf = false; } } }
public new void CleanCheckpoints() { if (string.IsNullOrEmpty(SID) && (ID_Unsafe < 0 || AreaData.Areas.Count <= ID_Unsafe)) { throw new Exception($"SaveData contains invalid AreaStats with no SID and out-of-range ID of {ID_Unsafe} / {AreaData.Areas.Count}"); } AreaData area = AreaData.Get(ID); for (int i = 0; i < Modes.Length; i++) { AreaMode areaMode = (AreaMode)i; AreaModeStats areaModeStats = Modes[i]; ModeProperties modeProperties = null; if (area.HasMode(areaMode)) { modeProperties = area.Mode[i]; } HashSet <string> checkpoints = new HashSet <string>(areaModeStats.Checkpoints); areaModeStats.Checkpoints.Clear(); if (modeProperties != null && modeProperties.Checkpoints != null) { foreach (CheckpointData checkpointData in modeProperties.Checkpoints) { if (checkpoints.Contains(checkpointData.Level)) { areaModeStats.Checkpoints.Add(checkpointData.Level); } } } } }
private static HashSet <string> _GetCheckpoints(SaveData save, AreaKey area) { // TODO: Maybe switch back to using SaveData.GetCheckpoints in the future? if (Celeste.PlayMode == Celeste.PlayModes.Event) { return(new HashSet <string>()); } HashSet <string> set; AreaData areaData = AreaData.Areas[area.ID]; ModeProperties mode = areaData.Mode[(int)area.Mode]; if (save.DebugMode || save.CheatMode) { set = new HashSet <string>(); if (mode.Checkpoints != null) { foreach (CheckpointData cp in mode.Checkpoints) { set.Add($"{(AreaData.Get(cp.GetArea()) ?? areaData).GetSID()}|{cp.Level}"); } } return(set); } AreaModeStats areaModeStats = save.Areas[area.ID].Modes[(int)area.Mode]; set = areaModeStats.Checkpoints; // Perform the same "cleanup" as SaveData.GetCheckpoints, but copy the set when adding area SIDs. if (mode == null) { set.Clear(); return(set); } set.RemoveWhere((string a) => !mode.Checkpoints.Any((CheckpointData b) => b.Level == a)); AreaData[] subs = AreaData.Areas.Where(other => other.GetMeta()?.Parent == areaData.GetSID() && other.HasMode(area.Mode) ).ToArray(); return(new HashSet <string>(set.Select(s => { foreach (AreaData sub in subs) { foreach (CheckpointData cp in sub.Mode[(int)area.Mode].Checkpoints) { if (cp.Level == s) { return $"{sub.GetSID()}|{s}"; } } } return s; }))); }
private static int OnChapterPanelGetModeHeight(On.Celeste.OuiChapterPanel.orig_GetModeHeight orig, OuiChapterPanel self) { // force the chapter panel to be bigger if deaths > 0 (we force deaths to display even if the player didn't beat the map) or if there is a speed berry PB, // because in these cases we have stuff to display in the chapter panel, and vanilla wouldn't display anything. AreaModeStats areaModeStats = self.RealStats.Modes[(int)self.Area.Mode]; if (Engine.Scene == overworldWrapper?.Scene && !AreaData.Get(self.Area).Interlude_Safe && (areaModeStats.Deaths > 0 || CollabModule.Instance.SaveData.SpeedBerryPBs.ContainsKey(self.Area.GetSID()))) { return(540); } return(orig(self)); }
private static void onRegisterCompletion(On.Celeste.SaveData.orig_RegisterCompletion orig, SaveData self, Session session) { orig(self, session); AreaKey currentArea = session.Area; if (IsHeartSide(currentArea.GetSID())) { string lobby = GetLobbyForLevelSet(currentArea.GetLevelSet()); if (lobby != null) { // completing the heart side should also complete the lobby. AreaModeStats areaModeStats = SaveData.Instance.Areas_Safe[AreaData.Get(lobby).ID].Modes[0]; areaModeStats.Completed = true; } } }
private static void OnChapterPanelUpdateStats(On.Celeste.OuiChapterPanel.orig_UpdateStats orig, OuiChapterPanel self, bool wiggle, bool?overrideStrawberryWiggle, bool?overrideDeathWiggle, bool?overrideHeartWiggle) { orig(self, wiggle, overrideStrawberryWiggle, overrideDeathWiggle, overrideHeartWiggle); DeathsCounter deathsCounter = new DynData <OuiChapterPanel>(self).Get <DeathsCounter>("deaths"); if (Engine.Scene == overworldWrapper?.Scene) { // within lobbies, death counts always show up, even if you didn't beat the map yet. AreaModeStats areaModeStats = self.DisplayedStats.Modes[(int)self.Area.Mode]; deathsCounter.Visible = areaModeStats.Deaths > 0 && !AreaData.Get(self.Area).Interlude_Safe; } // mod the death icon: for the path, use the current level set, or for lobbies, the lobby's matching level set. string pathToSkull = "CollabUtils2/skulls/" + self.Area.GetLevelSet(); if (LobbyHelper.GetLobbyLevelSet(self.Area.GetSID()) != null) { pathToSkull = "CollabUtils2/skulls/" + LobbyHelper.GetLobbyLevelSet(self.Area.GetSID()); } if (GFX.Gui.Has(pathToSkull)) { new DynData <DeathsCounter>(deathsCounter)["icon"] = GFX.Gui[pathToSkull]; } new DynData <DeathsCounter>(deathsCounter)["modifiedByCollabUtils"] = GFX.Gui.Has(pathToSkull); if (isPanelShowingLobby(self) || Engine.Scene == overworldWrapper?.Scene) { // turn strawberry counter into golden if there only are golden berries in the map MapData mapData = AreaData.Get(self.Area).Mode[0].MapData; if (mapData.GetDetectedStrawberriesIncludingUntracked() == mapData.Goldenberries.Count) { StrawberriesCounter strawberriesCounter = new DynData <OuiChapterPanel>(self).Get <StrawberriesCounter>("strawberries"); strawberriesCounter.Golden = true; strawberriesCounter.ShowOutOf = false; } } }
private static void CmdHearts(int amount = int.MaxValue, string levelSet = null) { patch_SaveData saveData = SaveData.Instance as patch_SaveData; if (saveData == null) { return; } if (string.IsNullOrEmpty(levelSet)) { levelSet = saveData.GetLevelSet(); } int num = 0; foreach (patch_AreaStats areaStats in saveData.Areas_Safe.Cast <patch_AreaStats>().Where(stats => stats.LevelSet == levelSet)) { for (int i = 0; i < areaStats.Modes.Length; i++) { if (AreaData.Get(areaStats.ID).Mode is not { } mode || mode.Length <= i || mode[i]?.MapData == null) { continue; } AreaModeStats areaModeStats = areaStats.Modes[i]; if (num < amount) { areaModeStats.HeartGem = true; num++; } else { areaModeStats.HeartGem = false; } } } }
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); } } 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; } } } } }