public static new void Load() { orig_Load(); // assign SIDs and CheckpointData.Area for vanilla maps. foreach (AreaData area in Areas) { area.SetSID("Celeste/" + area.Mode[0].Path); for (int modeId = 0; modeId < area.Mode.Length; modeId++) { ModeProperties mode = area.Mode[modeId]; if (mode?.Checkpoints == null) { continue; } foreach (CheckpointData checkpoint in mode.Checkpoints) { checkpoint.SetArea(area.ToKey((AreaMode)modeId)); } } } // Separate array as we sort it afterwards. List <AreaData> modAreas = new List <AreaData>(); lock (Everest.Content.Map) { foreach (ModAsset asset in Everest.Content.Map.Values.Where(asset => asset.Type == typeof(AssetTypeMap))) { string path = asset.PathVirtual.Substring(5); AreaData area = new AreaData(); // Default values. area.SetSID(path); area.Name = path; area.Icon = "areas/" + path.ToLowerInvariant(); if (!GFX.Gui.Has(area.Icon)) { area.Icon = "areas/null"; } area.Interlude = false; area.CanFullClear = true; area.TitleBaseColor = Calc.HexToColor("6c7c81"); area.TitleAccentColor = Calc.HexToColor("2f344b"); area.TitleTextColor = Color.White; area.IntroType = Player.IntroTypes.WakeUp; area.Dreaming = false; area.ColorGrade = null; area.Mode = new ModeProperties[] { new ModeProperties { Inventory = PlayerInventory.Default, AudioState = new AudioState(SFX.music_city, SFX.env_amb_00_main) } }; area.Wipe = (Scene scene, bool wipeIn, Action onComplete) => new AngledWipe(scene, wipeIn, onComplete); area.DarknessAlpha = 0.05f; area.BloomBase = 0f; area.BloomStrength = 1f; area.Jumpthru = "wood"; area.CassseteNoteColor = Calc.HexToColor("33a9ee"); area.CassetteSong = SFX.cas_01_forsaken_city; // Custom values can be set via the MapMeta. MapMeta meta = new MapMeta(); meta.ApplyTo(area); MapMeta metaLoaded = asset.GetMeta <MapMeta>(); if (metaLoaded != null) { area.SetMeta(null); metaLoaded.ApplyTo(area); meta = metaLoaded; } if (string.IsNullOrEmpty(area.Mode[0].Path)) { area.Mode[0].Path = asset.PathVirtual.Substring(5); } // Some of the game's code checks for [1] / [2] explicitly. // Let's just provide null modes to fill any gaps. meta.Modes = meta.Modes ?? new MapMetaModeProperties[3]; if (meta.Modes.Length < 3) { MapMetaModeProperties[] larger = new MapMetaModeProperties[3]; for (int i = 0; i < meta.Modes.Length; i++) { larger[i] = meta.Modes[i]; } meta.Modes = larger; } if (area.Mode.Length < 3) { ModeProperties[] larger = new ModeProperties[3]; for (int i = 0; i < area.Mode.Length; i++) { larger[i] = area.Mode[i]; } area.Mode = larger; } // Celeste levelset always appears first. if (area.GetLevelSet() == "Celeste") { Areas.Add(area); } else { modAreas.Add(area); } // Some special handling. area.OnLevelBegin = (level) => { MapMeta levelMeta = AreaData.Get(level.Session).GetMeta(); MapMetaModeProperties levelMetaMode = level.Session.MapData.GetMeta(); if (levelMetaMode?.SeekerSlowdown ?? false) { level.Add(new SeekerEffectsController()); } }; } } // Merge modAreas into Areas. Areas.AddRange(modAreas); // Find duplicates and remove any earlier copies. for (int i = 0; i < Areas.Count; i++) { AreaData area = Areas[i]; int otherIndex = Areas.FindIndex(other => other.GetSID() == area.GetSID()); if (otherIndex < i) { Areas[otherIndex] = area; Areas.RemoveAt(i); i--; } } // Sort areas. Areas.Sort(AreaComparison); // Remove AreaDatas which are now a mode of another AreaData. // This can happen late as the map data (.bin) can contain additional metadata. for (int i = 0; i < Areas.Count; i++) { AreaData area = Areas[i]; string path = area.Mode[0].Path; int otherIndex = Areas.FindIndex(other => other.Mode.Any(otherMode => otherMode?.Path == path)); if (otherIndex != -1 && otherIndex != i) { Areas.RemoveAt(i); i--; continue; } int? order; AreaMode side; string name; ParseName(path, out order, out side, out name); // Also check for .bins possibly belonging to A side .bins by their path and lack of existing modes. for (int ii = 0; ii < Areas.Count; ii++) { AreaData other = Areas[ii]; int? otherOrder; AreaMode otherSide; string otherName; ParseName(other.Mode[0].Path, out otherOrder, out otherSide, out otherName); if (area.GetLevelSet() == other.GetLevelSet() && order == otherOrder && name == otherName && side != otherSide && !other.HasMode(side)) { if (other.Mode[(int)side] == null) { other.Mode[(int)side] = new ModeProperties { Inventory = PlayerInventory.Default, AudioState = new AudioState(SFX.music_city, SFX.env_amb_00_main) } } ; other.Mode[(int)side].Path = path; Areas.RemoveAt(i); i--; break; } } } for (int i = 0; i < Areas.Count; i++) { AreaData area = Areas[i]; area.ID = i; // Clean up non-existing modes. int modei = 0; for (; modei < area.Mode.Length; modei++) { ModeProperties mode = area.Mode[modei]; if (mode == null || string.IsNullOrEmpty(mode.Path)) { break; } } Array.Resize(ref area.Mode, modei); Logger.Log("AreaData", $"{i}: {area.GetSID()} - {area.Mode.Length} sides"); // Update old MapData areas and load any new areas. // Add the A side MapData or update its area key. if (area.Mode[0].MapData != null) { area.Mode[0].MapData.Area = area.ToKey(); } else { area.Mode[0].MapData = new MapData(area.ToKey()); } if (area.IsInterludeUnsafe()) { continue; } // A and (some) B sides have PoemIDs. Can be overridden via empty PoemID. if (area.Mode[0].PoemID == null) { area.Mode[0].PoemID = area.GetSID().DialogKeyify() + "_A"; } if (area.Mode.Length > 1 && area.Mode[1] != null && area.Mode[1].PoemID == null) { area.Mode[1].PoemID = area.GetSID().DialogKeyify() + "_B"; } // Update all other existing mode's area keys. for (int mode = 1; mode < area.Mode.Length; mode++) { if (area.Mode[mode] == null) { continue; } if (area.Mode[mode].MapData != null) { area.Mode[mode].MapData.Area = area.ToKey((AreaMode)mode); } else { area.Mode[mode].MapData = new MapData(area.ToKey((AreaMode)mode)); } } } // Load custom mountains // This needs to be done after areas are loaded because it depends on the MapMeta MTNExt.LoadMod(); MTNExt.LoadModData(); }