using Barotrauma.Extensions; using Barotrauma.Lights; using Barotrauma.RuinGeneration; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Microsoft.Xna.Framework.Input; #if DEBUG using System.IO; using System.Xml; #else using Barotrauma.IO; #endif namespace Barotrauma { class LevelEditorScreen : EditorScreen { public override Camera Cam { get; } private GUIFrame leftPanel, rightPanel, bottomPanel, topPanel; private Point prevResolution; private LevelGenerationParams selectedParams; private RuinGenerationParams selectedRuinGenerationParams; private OutpostGenerationParams selectedOutpostGenerationParams; private LevelObjectPrefab selectedLevelObject; private BackgroundCreaturePrefab selectedBackgroundCreature; private GUIListBox paramsList, ruinParamsList, caveParamsList, outpostParamsList, levelObjectList, backgroundCreatureList; private GUIListBox editorContainer; private GUIButton spriteEditDoneButton; private GUITextBox seedBox; private GUITickBox lightingEnabled, cursorLightEnabled, allowInvalidOutpost, mirrorLevel; private GUIDropDown selectedSubDropDown; private GUIDropDown selectedBeaconStationDropdown; private GUIDropDown selectedWreckDropdown; private GUINumberInput forceDifficultyInput; private Sprite editingSprite; private LightSource pointerLightSource; private readonly Color[] tunnelDebugColors = new Color[] { Color.White, Color.Cyan, Color.LightGreen, Color.Red, Color.LightYellow, Color.LightSeaGreen }; private LevelData currentLevelData; private void RefreshUI(bool forceCreate = false) { if (forceCreate) { CreateUI(); } GUI.PreventPauseMenuToggle = false; pointerLightSource = new LightSource(Vector2.Zero, 1000.0f, Color.White, submarine: null); GameMain.LightManager.AddLight(pointerLightSource); topPanel.ClearChildren(); new SerializableEntityEditor(topPanel.RectTransform, pointerLightSource.LightSourceParams, false, true); editingSprite = null; UpdateParamsList(); UpdateRuinParamsList(); UpdateCaveParamsList(); UpdateOutpostParamsList(); UpdateLevelObjectsList(); UpdateBackgroundCreatureList(); } private void CreateUI() { Frame.ClearChildren(); leftPanel?.ClearChildren(); rightPanel?.ClearChildren(); leftPanel = new GUIFrame(new RectTransform(new Vector2(0.125f, 0.8f), Frame.RectTransform) { MinSize = new Point(150, 0) }); var paddedLeftPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.9f, 0.95f), leftPanel.RectTransform, Anchor.CenterLeft) { RelativeOffset = new Vector2(0.02f, 0.0f) }) { Stretch = true, RelativeSpacing = 0.01f }; paramsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.3f), paddedLeftPanel.RectTransform)) { PlaySoundOnSelect = true }; paramsList.OnSelected += (GUIComponent component, object obj) => { selectedParams = obj as LevelGenerationParams; currentLevelData = LevelData.CreateRandom(seedBox.Text, generationParams: selectedParams); editorContainer.ClearChildren(); SortLevelObjectsList(currentLevelData); SortBackgroundCreaturesList(currentLevelData); new SerializableEntityEditor(editorContainer.Content.RectTransform, selectedParams, inGame: false, showName: true, elementHeight: 20, titleFont: GUIStyle.LargeFont); forceDifficultyInput.FloatValue = (selectedParams.MinLevelDifficulty + selectedParams.MaxLevelDifficulty) / 2f; return true; }; var ruinTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.ruinparams"), font: GUIStyle.SubHeadingFont); ruinParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) { PlaySoundOnSelect = true }; ruinParamsList.OnSelected += (GUIComponent component, object obj) => { if (selectedRuinGenerationParams == obj) { // need to wait a frame before deselecting or the highlight on the list item gets left on CoroutineManager.StartCoroutine(DeselectRuinParams()); IEnumerable DeselectRuinParams() { if (Screen.Selected != this) { yield break; } yield return null; selectedRuinGenerationParams = null; CreateOutpostGenerationParamsEditor(null); ruinParamsList.Deselect(); } } else { selectedRuinGenerationParams = obj as RuinGenerationParams; CreateOutpostGenerationParamsEditor(selectedRuinGenerationParams); } return true; }; var caveTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.caveparams"), font: GUIStyle.SubHeadingFont); caveParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.1f), paddedLeftPanel.RectTransform)) { PlaySoundOnSelect = true }; caveParamsList.OnSelected += (GUIComponent component, object obj) => { CreateCaveParamsEditor(obj as CaveGenerationParams); return true; }; var outpostTitle = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.outpostparams"), font: GUIStyle.SubHeadingFont); GUITextBlock.AutoScaleAndNormalize(ruinTitle, caveTitle, outpostTitle); outpostParamsList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.2f), paddedLeftPanel.RectTransform)) { PlaySoundOnSelect = true }; outpostParamsList.OnSelected += (GUIComponent component, object obj) => { selectedOutpostGenerationParams = obj as OutpostGenerationParams; CreateOutpostGenerationParamsEditor(selectedOutpostGenerationParams); return true; }; var createLevelObjButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.createlevelobj")) { OnClicked = (btn, obj) => { Wizard.Instance.Create(); return true; } }; GUITextBlock.AutoScaleAndNormalize(createLevelObjButton.TextBlock); lightingEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.lightingenabled")); cursorLightEnabled = new GUITickBox(new RectTransform(new Vector2(1.0f, 0.025f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.cursorlightenabled")); new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedLeftPanel.RectTransform), TextManager.Get("leveleditor.reloadtextures")) { OnClicked = (btn, obj) => { Level.Loaded?.ReloadTextures(); return true; } }; new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedLeftPanel.RectTransform), TextManager.Get("editor.saveall")) { OnClicked = (btn, obj) => { SerializeAll(); GUI.AddMessage(TextManager.Get("leveleditor.allsaved"), GUIStyle.Green); return true; } }; rightPanel = new GUIFrame(new RectTransform(new Vector2(0.25f, 1.0f), Frame.RectTransform, Anchor.TopRight) { MinSize = new Point(450, 0) }); var paddedRightPanel = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.95f), rightPanel.RectTransform, Anchor.Center) { RelativeOffset = new Vector2(0.02f, 0.0f) }) { Stretch = true, RelativeSpacing = 0.01f }; editorContainer = new GUIListBox(new RectTransform(new Vector2(1.0f, 1.0f), paddedRightPanel.RectTransform)); var seedContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedRightPanel.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft); Vector2 randomizeButtonRelativeSize = GetRandomizeButtonRelativeSize(); Vector2 elementRelativeSize = GetSeedElementRelativeSize(); var seedLabel = new GUITextBlock(new RectTransform(elementRelativeSize, seedContainer.RectTransform), TextManager.Get("leveleditor.levelseed")); seedBox = new GUITextBox(new RectTransform(elementRelativeSize, seedContainer.RectTransform), GetLevelSeed()); var seedButton = new GUIButton(new RectTransform(randomizeButtonRelativeSize, seedContainer.RectTransform), style: "RandomizeButton") { OnClicked = (button, userData) => { if (seedBox == null) { return false; } seedBox.Text = GetLevelSeed(); return true; } }; seedContainer.RectTransform.SizeChanged += () => { Vector2 randomizeButtonRelativeSize = GetRandomizeButtonRelativeSize(); Vector2 elementRelativeSize = GetSeedElementRelativeSize(); seedLabel.RectTransform.RelativeSize = elementRelativeSize; seedBox.RectTransform.RelativeSize = elementRelativeSize; seedButton.RectTransform.RelativeSize = randomizeButtonRelativeSize; }; Vector2 GetRandomizeButtonRelativeSize() => 0.2f * seedContainer.Rect.Width > seedContainer.Rect.Height ? new Vector2(Math.Min((float)seedContainer.Rect.Height / seedContainer.Rect.Width, 0.2f), 1.0f) : new Vector2(0.15f, Math.Min((0.2f * seedContainer.Rect.Width) / seedContainer.Rect.Height, 1.0f)); Vector2 GetSeedElementRelativeSize() => new Vector2(0.5f * (1.0f - randomizeButtonRelativeSize.X), 1.0f); static string GetLevelSeed() => ToolBox.RandomSeed(8); var subDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform), TextManager.Get("submarine")); selectedSubDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), subDropDownContainer.RectTransform)); foreach (SubmarineInfo sub in SubmarineInfo.SavedSubmarines) { if (sub.Type != SubmarineType.Player) { continue; } selectedSubDropDown.AddItem(sub.DisplayName, userData: sub); } subDropDownContainer.RectTransform.MinSize = new Point(0, selectedSubDropDown.RectTransform.MinSize.Y); var beaconStationDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), beaconStationDropDownContainer.RectTransform), TextManager.Get("submarinetype.beaconstation")); selectedBeaconStationDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), beaconStationDropDownContainer.RectTransform)); selectedBeaconStationDropdown.AddItem(TextManager.Get("Any"), userData: null); foreach (SubmarineInfo beaconStation in SubmarineInfo.SavedSubmarines) { if (beaconStation.Type != SubmarineType.BeaconStation) { continue; } selectedBeaconStationDropdown.AddItem(beaconStation.DisplayName, userData: beaconStation); } beaconStationDropDownContainer.RectTransform.MinSize = new Point(0, selectedBeaconStationDropdown.RectTransform.MinSize.Y); var wreckDropDownContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), wreckDropDownContainer.RectTransform), TextManager.Get("submarinetype.wreck")); selectedWreckDropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), wreckDropDownContainer.RectTransform)); selectedWreckDropdown.AddItem(TextManager.Get("Any"), userData: null); foreach (SubmarineInfo wreck in SubmarineInfo.SavedSubmarines) { if (wreck.Type != SubmarineType.Wreck) { continue; } selectedWreckDropdown.AddItem(wreck.DisplayName, userData: wreck); } wreckDropDownContainer.RectTransform.MinSize = new Point(0, selectedWreckDropdown.RectTransform.MinSize.Y); var forceDifficultyContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.02f), paddedRightPanel.RectTransform), isHorizontal: true); new GUITextBlock(new RectTransform(new Vector2(0.5f, 1.0f), forceDifficultyContainer.RectTransform), TextManager.Get("leveldifficulty")); forceDifficultyInput = new GUINumberInput(new RectTransform(new Vector2(0.5f, 1.0f), forceDifficultyContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, FloatValue = Level.ForcedDifficulty ?? selectedParams?.MinLevelDifficulty ?? 0f, OnValueChanged = (numberInput) => { if (Level.ForcedDifficulty == null) { return; } Level.ForcedDifficulty = numberInput.FloatValue; } }; forceDifficultyContainer.RectTransform.MinSize = new Point(0, forceDifficultyInput.RectTransform.MinSize.Y); var tickBoxContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.04f), paddedRightPanel.RectTransform), isHorizontal: true); mirrorLevel = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.02f), tickBoxContainer.RectTransform), TextManager.Get("mirrorentityx")); allowInvalidOutpost = new GUITickBox(new RectTransform(new Vector2(0.5f, 0.025f), tickBoxContainer.RectTransform), TextManager.Get("leveleditor.allowinvalidoutpost")) { ToolTip = TextManager.Get("leveleditor.allowinvalidoutpost.tooltip") }; new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.generate")) { OnClicked = (btn, obj) => { bool wasLevelLoaded = Level.Loaded != null; Submarine.Unload(); if (selectedSubDropDown.SelectedData is SubmarineInfo subInfo) { Submarine.MainSub = new Submarine(subInfo); } GameMain.LightManager.ClearLights(); currentLevelData = LevelData.CreateRandom(seedBox.Text, difficulty: forceDifficultyInput.FloatValue, generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.ForceBeaconStation = selectedBeaconStationDropdown.SelectedData as SubmarineInfo; currentLevelData.ForceWreck = selectedWreckDropdown.SelectedData as SubmarineInfo; currentLevelData.ForceRuinGenerationParams = selectedRuinGenerationParams; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); UpdateBackgroundCreatureList(); if (Submarine.MainSub != null) { Vector2 startPos = Level.Loaded.StartPosition; if (Level.Loaded.StartOutpost != null) { startPos.Y -= Level.Loaded.StartOutpost.Borders.Height / 2 + Submarine.MainSub.Borders.Height / 2; } Submarine.MainSub?.SetPosition(startPos); } GameMain.LightManager.AddLight(pointerLightSource); if (!wasLevelLoaded || Cam.Position.X < 0 || Cam.Position.Y < 0 || Cam.Position.Y > Level.Loaded.Size.X || Cam.Position.Y > Level.Loaded.Size.Y) { Cam.Position = new Vector2(Level.Loaded.Size.X / 2, Level.Loaded.Size.Y / 2); } foreach (GUITextBlock param in paramsList.Content.Children) { param.TextColor = param.UserData == selectedParams ? GUIStyle.Green : param.Style.TextColor; } seedBox.Deselect(); return true; } }; new GUIButton(new RectTransform(new Vector2(1.0f, 0.05f), paddedRightPanel.RectTransform), TextManager.Get("leveleditor.test")) { OnClicked = (btn, obj) => { if (Level.Loaded?.LevelData == null) { return false; } GameMain.GameScreen.Select(); var currEntities = Entity.GetEntities().ToList(); if (Submarine.MainSub != null) { var toRemove = Entity.GetEntities().Where(e => e.Submarine == Submarine.MainSub).ToList(); foreach (Entity ent in toRemove) { ent.Remove(); } Submarine.MainSub.Remove(); } var nonPlayerFiles = ContentPackageManager.EnabledPackages.All.SelectMany(p => p .GetFiles() .Where(f => f is not SubmarineFile)).ToArray(); SubmarineInfo subInfo = selectedSubDropDown.SelectedData as SubmarineInfo; subInfo ??= SubmarineInfo.SavedSubmarines.GetRandomUnsynced(s => s.IsPlayer && !s.HasTag(SubmarineTag.Shuttle) && !nonPlayerFiles.Any(f => f.Path == s.FilePath)); GameSession gameSession = new GameSession(subInfo, Option.None, CampaignDataPath.Empty, GameModePreset.TestMode, CampaignSettings.Empty, null); gameSession.StartRound(Level.Loaded.LevelData, mirrorLevel.Selected); (gameSession.GameMode as TestGameMode).OnRoundEnd = () => { GameMain.LevelEditorScreen.Select(); Submarine.MainSub.Remove(); var toRemove = Entity.GetEntities().Where(e => !currEntities.Contains(e)).ToList(); foreach (Entity ent in toRemove) { ent.Remove(); } Submarine.MainSub = null; }; GameMain.GameSession = gameSession; return true; } }; bottomPanel = new GUIFrame(new RectTransform(new Vector2(0.75f, 0.22f), Frame.RectTransform, Anchor.BottomLeft) { MaxSize = new Point(GameMain.GraphicsWidth - rightPanel.Rect.Width, 1000) }, style: "GUIFrameBottom"); var bottomPanelContents = new GUILayoutGroup(new RectTransform(new Vector2(0.98f, 0.9f), bottomPanel.RectTransform, Anchor.Center)) { Stretch = true }; var bottomPanelButtons = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.1f), bottomPanelContents.RectTransform), isHorizontal: true); new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), bottomPanelButtons.RectTransform), TextManager.Get("leveleditor.levelobjects"), style: "GUITabButton") { Selected = true, OnClicked = (btn, __) => { bottomPanelButtons.Children.ForEach(c => c.Selected = c == btn); levelObjectList.Visible = true; levelObjectList.IgnoreLayoutGroups = false; backgroundCreatureList.Visible = false; backgroundCreatureList.IgnoreLayoutGroups = true; return true; } }; new GUIButton(new RectTransform(new Vector2(0.25f, 1.0f), bottomPanelButtons.RectTransform), TextManager.Get("leveleditor.backgroundcreatures"), style: "GUITabButton") { OnClicked = (btn, __) => { bottomPanelButtons.Children.ForEach(c => c.Selected = c == btn); backgroundCreatureList.Visible = true; backgroundCreatureList.IgnoreLayoutGroups = false; levelObjectList.Visible = false; levelObjectList.IgnoreLayoutGroups = true; return true; } }; bottomPanelButtons.RectTransform.NonScaledSize = new Point(bottomPanelButtons.Rect.Width, bottomPanelButtons.Children.First().Rect.Height); levelObjectList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.85f), bottomPanelContents.RectTransform)) { PlaySoundOnSelect = true, UseGridLayout = true }; levelObjectList.OnSelected += (GUIComponent component, object obj) => { selectedLevelObject = obj as LevelObjectPrefab; CreateLevelObjectEditor(selectedLevelObject); return true; }; backgroundCreatureList = new GUIListBox(new RectTransform(new Vector2(1.0f, 0.85f), bottomPanelContents.RectTransform)) { PlaySoundOnSelect = true, UseGridLayout = true, Visible = false, IgnoreLayoutGroups = true }; backgroundCreatureList.OnSelected += (GUIComponent component, object obj) => { selectedBackgroundCreature = obj as BackgroundCreaturePrefab; CreateBackgroundCreatureEditor(selectedBackgroundCreature); return true; }; spriteEditDoneButton = new GUIButton(new RectTransform(new Point(200, 30), anchor: Anchor.BottomRight) { AbsoluteOffset = new Point(20, 20) }, TextManager.Get("leveleditor.spriteeditdone")) { OnClicked = (btn, userdata) => { editingSprite = null; return true; } }; topPanel = new GUIFrame(new RectTransform(new Point(400, 100), GUI.Canvas) { RelativeOffset = new Vector2(leftPanel.RectTransform.RelativeSize.X * 2, 0.0f) }, style: "GUIFrameTop"); prevResolution = new Point(GameMain.GraphicsWidth, GameMain.GraphicsHeight); } public LevelEditorScreen() { Cam = new Camera() { MinZoom = 0.01f, MaxZoom = 1.0f }; RefreshUI(forceCreate: true); } public void TestLevelGenerationForErrors(int amountOfLevelsToGenerate) { if (selectedParams == null) { throw new InvalidOperationException("No level generation parameters selected in the level editor."); } CoroutineManager.StartCoroutine(GenerateLevels()); IEnumerable GenerateLevels() { using var errorCatcher = DebugConsole.ErrorCatcher.Create(); for (int i = 0; i < amountOfLevelsToGenerate; i++) { Submarine.Unload(); GameMain.LightManager.ClearLights(); currentLevelData = LevelData.CreateRandom(ToolBox.RandomSeed(10), generationParams: selectedParams); currentLevelData.ForceOutpostGenerationParams = outpostParamsList.SelectedData as OutpostGenerationParams; currentLevelData.ForceBeaconStation = selectedBeaconStationDropdown.SelectedData as SubmarineInfo; currentLevelData.ForceWreck = selectedWreckDropdown.SelectedData as SubmarineInfo; currentLevelData.AllowInvalidOutpost = allowInvalidOutpost.Selected; var dummyLocations = GameSession.CreateDummyLocations(currentLevelData); DebugConsole.NewMessage("*****************************************************************************"); DebugConsole.NewMessage($"Generating level {(i + 1)}/{amountOfLevelsToGenerate}: "); DebugConsole.NewMessage(" Seed: " + currentLevelData.Seed); DebugConsole.NewMessage(" Outpost parameters: " + (currentLevelData.ForceOutpostGenerationParams?.Name ?? "None")); DebugConsole.NewMessage(" Level generation params: " + selectedParams.Identifier); DebugConsole.NewMessage(" Mirrored: " + mirrorLevel.Selected); DebugConsole.NewMessage(" Adjacent locations: " + (dummyLocations[0]?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (dummyLocations[1]?.Type.Identifier ?? "none".ToIdentifier())); yield return CoroutineStatus.Running; Level.Generate(currentLevelData, mirror: mirrorLevel.Selected, startLocation: dummyLocations[0], endLocation: dummyLocations[1]); Submarine.MainSub?.SetPosition(Level.Loaded.StartPosition); GameMain.LightManager.AddLight(pointerLightSource); seedBox.Deselect(); if (errorCatcher.Errors.Any()) { DebugConsole.ThrowError("Error while generating level:"); errorCatcher.Errors.ToList().ForEach(e => DebugConsole.ThrowError(e.Text)); yield return CoroutineStatus.Success; } yield return new WaitForSeconds(0.1f); } } } public override void Select() { base.Select(); RefreshUI(forceCreate: false); } protected override void DeselectEditorSpecific() { pointerLightSource?.Remove(); pointerLightSource = null; } private void UpdateParamsList() { editorContainer.ClearChildren(); paramsList.Content.ClearChildren(); foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), paramsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Identifier.Value) { Padding = Vector4.Zero, UserData = genParams }; } } private void UpdateCaveParamsList() { editorContainer.ClearChildren(); caveParamsList.Content.ClearChildren(); foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), caveParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) { Padding = Vector4.Zero, UserData = genParams }; } } private void UpdateRuinParamsList() { editorContainer.ClearChildren(); ruinParamsList.Content.ClearChildren(); foreach (RuinGenerationParams genParams in RuinGenerationParams.RuinParams.OrderBy(p => p.Identifier)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), ruinParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Identifier.Value) { Padding = Vector4.Zero, UserData = genParams }; } } private void UpdateOutpostParamsList() { editorContainer.ClearChildren(); outpostParamsList.Content.ClearChildren(); foreach (OutpostGenerationParams genParams in OutpostGenerationParams.OutpostParams.OrderBy(p => p.Name)) { new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.05f), outpostParamsList.Content.RectTransform) { MinSize = new Point(0, 20) }, genParams.Name) { Padding = Vector4.Zero, UserData = genParams }; } } private void UpdateLevelObjectsList() { editorContainer.ClearChildren(); levelObjectList.Content.ClearChildren(); int objectsPerRow = (int)Math.Ceiling(levelObjectList.Content.Rect.Width / Math.Max(100 * GUI.Scale, 100)); float relWidth = 1.0f / objectsPerRow; foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.Prefabs) { var frame = new GUIFrame(new RectTransform( new Vector2(relWidth, relWidth * ((float)levelObjectList.Content.Rect.Width / levelObjectList.Content.Rect.Height)), levelObjectList.Content.RectTransform) { MinSize = new Point(0, 60) }, style: "ListBoxElementSquare") { UserData = levelObjPrefab, ToolTip = levelObjPrefab.Name }; var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null) { CanBeFocused = false }; GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), text: ToolBox.LimitString(levelObjPrefab.Name, GUIStyle.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false, ToolTip = levelObjPrefab.Name }; Sprite sprite = levelObjPrefab.Sprites.FirstOrDefault() ?? levelObjPrefab.DeformableSprite?.Sprite; new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height, paddedFrame.Rect.Height - textBlock.Rect.Height), paddedFrame.RectTransform, Anchor.TopCenter), sprite, scaleToFit: true) { LoadAsynchronously = true, CanBeFocused = false }; } } private void UpdateBackgroundCreatureList() { editorContainer.ClearChildren(); backgroundCreatureList.Content.ClearChildren(); int objectsPerRow = (int)Math.Ceiling(backgroundCreatureList.Content.Rect.Width / Math.Max(100 * GUI.Scale, 100)); float relWidth = 1.0f / objectsPerRow; foreach (BackgroundCreaturePrefab backgroundCreaturePrefab in BackgroundCreaturePrefab.Prefabs) { var frame = new GUIFrame(new RectTransform( new Vector2(relWidth, relWidth * ((float)backgroundCreatureList.Content.Rect.Width / backgroundCreatureList.Content.Rect.Height)), backgroundCreatureList.Content.RectTransform) { MinSize = new Point(0, 60) }, style: "ListBoxElementSquare") { UserData = backgroundCreaturePrefab }; var paddedFrame = new GUIFrame(new RectTransform(new Vector2(0.9f, 0.9f), frame.RectTransform, Anchor.Center), style: null); GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), paddedFrame.RectTransform, Anchor.BottomCenter), text: ToolBox.LimitString(backgroundCreaturePrefab.Name, GUIStyle.SmallFont, paddedFrame.Rect.Width), textAlignment: Alignment.Center, font: GUIStyle.SmallFont) { CanBeFocused = false, ToolTip = backgroundCreaturePrefab.Name }; Sprite sprite = backgroundCreaturePrefab.Sprite ?? backgroundCreaturePrefab.DeformableSprite?.Sprite; new GUIImage(new RectTransform(new Point(paddedFrame.Rect.Height, paddedFrame.Rect.Height - textBlock.Rect.Height), paddedFrame.RectTransform, Anchor.TopCenter), sprite, scaleToFit: true) { LoadAsynchronously = true, CanBeFocused = false }; } SortBackgroundCreaturesList(currentLevelData); } private void CreateCaveParamsEditor(CaveGenerationParams caveGenerationParams) { editorContainer.ClearChildren(); var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, caveGenerationParams, false, true, elementHeight: 20); if (selectedParams != null) { var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, isHorizontal: false, childAnchor: Anchor.TopCenter) { AbsoluteSpacing = 5, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", selectedParams.Identifier.Value), textAlignment: Alignment.Center); new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, FloatValue = caveGenerationParams.GetCommonness(currentLevelData, abyss: false), OnValueChanged = (numberInput) => { caveGenerationParams.OverrideCommonness[selectedParams.Identifier] = numberInput.FloatValue; } }; new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), commonnessContainer.RectTransform), style: null); editor.AddCustomContent(commonnessContainer, 1); } } private void CreateOutpostGenerationParamsEditor(OutpostGenerationParams outpostGenerationParams) { editorContainer.ClearChildren(); if (outpostGenerationParams == null) { return; } var outpostParamsEditor = new SerializableEntityEditor(editorContainer.Content.RectTransform, outpostGenerationParams, false, true, elementHeight: 20); // location type ------------------------- var locationTypeGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, 20)), isHorizontal: true, childAnchor: Anchor.CenterLeft) { Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), TextManager.Get("outpostmoduleallowedlocationtypes"), textAlignment: Alignment.CenterLeft); HashSet availableLocationTypes = new HashSet { "any".ToIdentifier() }; foreach (LocationType locationType in LocationType.Prefabs) { availableLocationTypes.Add(locationType.Identifier); } var locationTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1f), locationTypeGroup.RectTransform), text: LocalizedString.Join(", ", outpostGenerationParams.AllowedLocationTypes.Select(lt => TextManager.Capitalize(lt.Value)) ?? ((LocalizedString)"any").ToEnumerable()), selectMultiple: true); foreach (Identifier locationType in availableLocationTypes) { locationTypeDropDown.AddItem(TextManager.Capitalize(locationType.Value), locationType); if (outpostGenerationParams.AllowedLocationTypes.Contains(locationType)) { locationTypeDropDown.SelectItem(locationType); } } if (!outpostGenerationParams.AllowedLocationTypes.Any()) { locationTypeDropDown.SelectItem("any"); } locationTypeDropDown.AfterSelected += (_, __) => { outpostGenerationParams.SetAllowedLocationTypes(locationTypeDropDown.SelectedDataMultiple.Cast()); locationTypeDropDown.Text = ToolBox.LimitString(locationTypeDropDown.Text, locationTypeDropDown.Font, locationTypeDropDown.Rect.Width); return true; }; locationTypeGroup.RectTransform.MinSize = new Point(locationTypeGroup.Rect.Width, locationTypeGroup.RectTransform.Children.Max(c => c.MinSize.Y)); outpostParamsEditor.AddCustomContent(locationTypeGroup, 100); // module count ------------------------- foreach (var moduleCount in outpostGenerationParams.ModuleCounts) { var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, moduleCount, inGame: false, showName: true, elementHeight: 20, titleFont: GUIStyle.Font); foreach (var componentList in editor.Fields.Values) { foreach (var component in componentList) { if (component is GUINumberInput numberInput) { numberInput.OnValueChanged += (numInput) => { if (moduleCount.Count == 0) { //refresh to remove this module count from the editor outpostParamsList.Select(outpostParamsList.SelectedData); } }; } } } editor.RectTransform.MaxSize = new Point(int.MaxValue, editor.Rect.Height); outpostParamsEditor.AddCustomContent(editor, 100); editor.Recalculate(); } // add module count ------------------------- var addModuleCountGroup = new GUILayoutGroup(new RectTransform(new Point(editorContainer.Content.Rect.Width, (int)(40 * GUI.Scale))), isHorizontal: true, childAnchor: Anchor.Center); HashSet availableFlags = new HashSet(); foreach (Identifier flag in OutpostGenerationParams.OutpostParams.SelectMany(p => p.ModuleCounts.Select(m => m.Identifier))) { availableFlags.Add(flag); } foreach (var sub in SubmarineInfo.SavedSubmarines) { if (sub.OutpostModuleInfo == null) { continue; } foreach (Identifier flag in sub.OutpostModuleInfo.ModuleFlags) { availableFlags.Add(flag); } } var moduleTypeDropDown = new GUIDropDown(new RectTransform(new Vector2(0.8f, 0.8f), addModuleCountGroup.RectTransform), text: TextManager.Get("leveleditor.addmoduletype")); foreach (Identifier flag in availableFlags) { if (outpostGenerationParams.ModuleCounts.Any(mc => mc.Identifier == flag)) { continue; } moduleTypeDropDown.AddItem(TextManager.Capitalize(flag.Value), flag); } moduleTypeDropDown.OnSelected += (_, userdata) => { outpostGenerationParams.SetModuleCount((Identifier)userdata, 1); outpostParamsList.Select(outpostParamsList.SelectedData); return true; }; addModuleCountGroup.RectTransform.MinSize = new Point(addModuleCountGroup.Rect.Width, addModuleCountGroup.RectTransform.Children.Max(c => c.MinSize.Y)); outpostParamsEditor.AddCustomContent(addModuleCountGroup, 100); outpostParamsEditor.Recalculate(); } private void CreateLevelObjectEditor(LevelObjectPrefab levelObjectPrefab) { editorContainer.ClearChildren(); var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, levelObjectPrefab, false, true, elementHeight: 20, titleFont: GUIStyle.LargeFont); if (selectedParams != null) { List availableIdentifiers = new List(); if (selectedParams != null) { availableIdentifiers.Add(selectedParams.Identifier); } foreach (var caveParam in CaveGenerationParams.CaveParams) { if (selectedParams != null && caveParam.GetCommonness(currentLevelData, abyss: false) <= 0.0f) { continue; } availableIdentifiers.Add(caveParam.Identifier); } availableIdentifiers.Reverse(); foreach (Identifier paramsId in availableIdentifiers) { var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, isHorizontal: false, childAnchor: Anchor.TopCenter) { AbsoluteSpacing = 5, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId.Value), textAlignment: Alignment.Center); new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, FloatValue = selectedParams.Identifier == paramsId ? levelObjectPrefab.GetCommonness(currentLevelData) : levelObjectPrefab.GetCommonness(CaveGenerationParams.CaveParams.Find(p => p.Identifier == paramsId)), OnValueChanged = (numberInput) => { levelObjectPrefab.OverrideCommonness[paramsId] = numberInput.FloatValue; } }; new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), commonnessContainer.RectTransform), style: null); editor.AddCustomContent(commonnessContainer, 1); } } Sprite sprite = levelObjectPrefab.Sprites.FirstOrDefault() ?? levelObjectPrefab.DeformableSprite?.Sprite; if (sprite != null) { editor.AddCustomContent(new GUIButton(new RectTransform(new Point(editor.Rect.Width / 2, (int)(25 * GUI.Scale))) { IsFixedSize = true }, TextManager.Get("leveleditor.editsprite")) { OnClicked = (btn, userdata) => { editingSprite = sprite; GameMain.SpriteEditorScreen.SelectSprite(editingSprite); return true; } }, 1); } if (levelObjectPrefab.DeformableSprite != null) { var deformEditor = levelObjectPrefab.DeformableSprite.CreateEditor(editor, levelObjectPrefab.SpriteDeformations, levelObjectPrefab.Name); deformEditor.GetChild().OnSelected += (selected, userdata) => { CreateLevelObjectEditor(selectedLevelObject); return true; }; editor.AddCustomContent(deformEditor, editor.ContentCount); } //child object editing new GUITextBlock(new RectTransform(new Point(editor.Rect.Width, 40), editorContainer.Content.RectTransform), TextManager.Get("leveleditor.childobjects"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomCenter); foreach (LevelObjectPrefab.ChildObject childObj in levelObjectPrefab.ChildObjects) { var childObjFrame = new GUIFrame(new RectTransform(new Point(editor.Rect.Width, 30))); var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.95f, 0.9f), childObjFrame.RectTransform, Anchor.Center), isHorizontal: true) { Stretch = true, RelativeSpacing = 0.05f }; var selectedChildObj = childObj; var dropdown = new GUIDropDown(new RectTransform(new Vector2(0.5f, 1.0f), paddedFrame.RectTransform), elementCount: 10, selectMultiple: true); foreach (LevelObjectPrefab objPrefab in LevelObjectPrefab.Prefabs) { dropdown.AddItem(objPrefab.Name, objPrefab); if (childObj.AllowedNames.Contains(objPrefab.Name)) { dropdown.SelectItem(objPrefab); } } dropdown.AfterSelected = (selected, obj) => { childObj.AllowedNames = dropdown.SelectedDataMultiple.Select(d => ((LevelObjectPrefab)d).Name).ToList(); return true; }; new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 10, OnValueChanged = (numberInput) => { selectedChildObj.MinCount = numberInput.IntValue; selectedChildObj.MaxCount = Math.Max(selectedChildObj.MaxCount, selectedChildObj.MinCount); } }.IntValue = childObj.MinCount; new GUINumberInput(new RectTransform(new Vector2(0.2f, 1.0f), paddedFrame.RectTransform), NumberType.Int) { MinValueInt = 0, MaxValueInt = 10, OnValueChanged = (numberInput) => { selectedChildObj.MaxCount = numberInput.IntValue; selectedChildObj.MinCount = Math.Min(selectedChildObj.MaxCount, selectedChildObj.MinCount); } }.IntValue = childObj.MaxCount; new GUIButton(new RectTransform(new Vector2(0.1f, 1.0f), paddedFrame.RectTransform, scaleBasis: ScaleBasis.BothHeight), style: "GUICancelButton") { OnClicked = (btn, userdata) => { selectedLevelObject.ChildObjects.Remove(selectedChildObj); CreateLevelObjectEditor(selectedLevelObject); return true; } }; childObjFrame.RectTransform.Parent = editorContainer.Content.RectTransform; } var buttonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), editorContainer.Content.RectTransform), style: null); new GUIButton(new RectTransform(new Point(editor.Rect.Width / 2, 20), buttonContainer.RectTransform, Anchor.Center), TextManager.Get("leveleditor.addchildobject")) { OnClicked = (btn, userdata) => { selectedLevelObject.ChildObjects.Add(new LevelObjectPrefab.ChildObject()); CreateLevelObjectEditor(selectedLevelObject); return true; } }; buttonContainer.RectTransform.MinSize = buttonContainer.RectTransform.Children.First().MinSize; //light editing new GUITextBlock(new RectTransform(new Point(editor.Rect.Width, 40), editorContainer.Content.RectTransform), TextManager.Get("leveleditor.lightsources"), textAlignment: Alignment.BottomCenter, font: GUIStyle.SubHeadingFont); foreach (LightSourceParams lightSourceParams in selectedLevelObject.LightSourceParams) { new SerializableEntityEditor(editorContainer.Content.RectTransform, lightSourceParams, inGame: false, showName: true); } buttonContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), editorContainer.Content.RectTransform), style: null); new GUIButton(new RectTransform(new Point(editor.Rect.Width / 2, 20), buttonContainer.RectTransform, Anchor.Center), TextManager.Get("leveleditor.addlightsource")) { OnClicked = (btn, userdata) => { selectedLevelObject.LightSourceTriggerIndex.Add(-1); selectedLevelObject.LightSourceParams.Add(new LightSourceParams(100.0f, Color.White)); CreateLevelObjectEditor(selectedLevelObject); return true; } }; buttonContainer.RectTransform.MinSize = buttonContainer.RectTransform.Children.First().MinSize; } private void SortLevelObjectsList(LevelData levelData) { //fade out levelobjects that don't spawn in this type of level foreach (GUIComponent levelObjFrame in levelObjectList.Content.Children) { var levelObj = levelObjFrame.UserData as LevelObjectPrefab; float commonness = levelObj.GetCommonness(levelData); Color color = GUIStyle.Green; if (commonness > 0.0f && levelData?.GenerationParams != null) { if (levelObj.MinSurfaceWidth > levelData.GenerationParams.CellSubdivisionLength && levelObj.SpawnPos.HasFlag(LevelObjectPrefab.SpawnPosType.Wall)) { color = Color.Orange; levelObjFrame.ToolTip = $"Potential issue: the level walls in \"{levelData.GenerationParams.Identifier}\" are set to be subdivided every {levelData.GenerationParams.CellSubdivisionLength} pixels, but the level object requires wall segments of at least {levelObj.MinSurfaceWidth} px. The object may be rarer than intended (or fail to spawn at all) in the level."; } } levelObjFrame.Color = commonness > 0.0f ? color * 0.4f : Color.Transparent; levelObjFrame.SelectedColor = commonness > 0.0f ? color * 0.6f : Color.White * 0.5f; levelObjFrame.HoverColor = commonness > 0.0f ? color * 0.7f : Color.White * 0.6f; levelObjFrame.GetAnyChild().Color = commonness > 0.0f ? Color.White : Color.DarkGray; if (commonness <= 0.0f) { levelObjFrame.GetAnyChild().TextColor = Color.DarkGray; } } //sort the levelobjects according to commonness in this level levelObjectList.Content.RectTransform.SortChildren((c1, c2) => { var levelObj1 = c1.GUIComponent.UserData as LevelObjectPrefab; var levelObj2 = c2.GUIComponent.UserData as LevelObjectPrefab; return Math.Sign(levelObj2.GetCommonness(levelData) - levelObj1.GetCommonness(levelData)); }); } private void SortBackgroundCreaturesList(LevelData levelData) { if (levelData == null) { return; } //fade out levelobjects that don't spawn in this type of level foreach (GUIComponent child in backgroundCreatureList.Content.Children) { if (child.UserData is not BackgroundCreaturePrefab creature) { continue; } SetElementColorBasedOnCommonness(child, creature.GetCommonness(levelData)); } //sort the levelobjects according to commonness in this level backgroundCreatureList.Content.RectTransform.SortChildren((c1, c2) => { var creature1 = c1.GUIComponent.UserData as BackgroundCreaturePrefab; var creature2 = c2.GUIComponent.UserData as BackgroundCreaturePrefab; return Math.Sign(creature2.GetCommonness(levelData) - creature1.GetCommonness(levelData)); }); } private static void SetElementColorBasedOnCommonness(GUIComponent element, float commonness) { element.Color = commonness > 0.0f ? GUIStyle.Green * 0.4f : Color.Transparent; element.SelectedColor = commonness > 0.0f ? GUIStyle.Green * 0.6f : Color.White * 0.5f; element.HoverColor = commonness > 0.0f ? GUIStyle.Green * 0.7f : Color.White * 0.6f; element.GetAnyChild().Color = commonness > 0.0f ? Color.White : Color.DarkGray; if (commonness <= 0.0f) { element.GetAnyChild().TextColor = Color.DarkGray; } } private void CreateBackgroundCreatureEditor(BackgroundCreaturePrefab backgroundCreaturePrefab) { editorContainer.ClearChildren(); var editor = new SerializableEntityEditor(editorContainer.Content.RectTransform, backgroundCreaturePrefab, false, true, elementHeight: 20, titleFont: GUIStyle.LargeFont); if (selectedParams != null) { List availableIdentifiers = new List() { selectedParams.Identifier }; foreach (Identifier paramsId in availableIdentifiers) { var commonnessContainer = new GUILayoutGroup(new RectTransform(new Point(editor.Rect.Width, 70)) { IsFixedSize = true }, isHorizontal: false, childAnchor: Anchor.TopCenter) { AbsoluteSpacing = 5, Stretch = true }; new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.4f), commonnessContainer.RectTransform), TextManager.GetWithVariable("leveleditor.levelobjcommonness", "[leveltype]", paramsId.Value), textAlignment: Alignment.Center); new GUINumberInput(new RectTransform(new Vector2(0.5f, 0.4f), commonnessContainer.RectTransform), NumberType.Float) { MinValueFloat = 0, MaxValueFloat = 100, FloatValue = backgroundCreaturePrefab.GetCommonness(currentLevelData), OnValueChanged = (numberInput) => { backgroundCreaturePrefab.OverrideCommonness[paramsId] = numberInput.FloatValue; } }; new GUIFrame(new RectTransform(new Vector2(1.0f, 0.2f), commonnessContainer.RectTransform), style: null); editor.AddCustomContent(commonnessContainer, 1); } } Sprite sprite = backgroundCreaturePrefab.Sprite ?? backgroundCreaturePrefab.DeformableSprite?.Sprite; if (sprite != null) { editor.AddCustomContent(new GUIButton(new RectTransform(new Point(editor.Rect.Width / 2, (int)(25 * GUI.Scale))) { IsFixedSize = true }, TextManager.Get("leveleditor.editsprite")) { OnClicked = (btn, userdata) => { editingSprite = sprite; GameMain.SpriteEditorScreen.SelectSprite(editingSprite); return true; } }, 1); } if (backgroundCreaturePrefab.DeformableSprite != null) { var deformEditor = backgroundCreaturePrefab.DeformableSprite.CreateEditor(editor, backgroundCreaturePrefab.SpriteDeformations, backgroundCreaturePrefab.Name); deformEditor.GetChild().OnSelected += (selected, userdata) => { CreateBackgroundCreatureEditor(backgroundCreaturePrefab); return true; }; editor.AddCustomContent(deformEditor, editor.ContentCount); } } public override void AddToGUIUpdateList() { base.AddToGUIUpdateList(); rightPanel.Visible = leftPanel.Visible = bottomPanel.Visible = editingSprite == null; if (editingSprite != null) { GameMain.SpriteEditorScreen.TopPanel.AddToGUIUpdateList(); spriteEditDoneButton.AddToGUIUpdateList(); } else if (lightingEnabled.Selected && cursorLightEnabled.Selected) { topPanel.AddToGUIUpdateList(); } } public override void Draw(double deltaTime, GraphicsDevice graphics, SpriteBatch spriteBatch) { if (lightingEnabled.Selected) { GameMain.LightManager.RenderLightMap(graphics, spriteBatch, Cam); } graphics.Clear(Color.Black); if (Level.Loaded != null) { Level.Loaded.DrawBack(graphics, spriteBatch, Cam); Level.Loaded.DrawFront(spriteBatch, Cam); spriteBatch.Begin(SpriteSortMode.BackToFront, BlendState.NonPremultiplied, SamplerState.LinearWrap, DepthStencilState.DepthRead, transformMatrix: Cam.Transform); Level.Loaded.DrawDebugOverlay(spriteBatch, Cam); Submarine.Draw(spriteBatch, false); Submarine.DrawFront(spriteBatch); Submarine.DrawDamageable(spriteBatch, null); GUI.DrawRectangle(spriteBatch, new Rectangle(new Point(0, -Level.Loaded.Size.Y), Level.Loaded.Size), Color.Gray, thickness: (int)(1.0f / Cam.Zoom)); for (int i = 0; i < Level.Loaded.Tunnels.Count; i++) { var tunnel = Level.Loaded.Tunnels[i]; Color tunnelColor = tunnelDebugColors[i % tunnelDebugColors.Length] * 0.2f; for (int j = 1; j < tunnel.Nodes.Count; j++) { Vector2 start = new Vector2(tunnel.Nodes[j - 1].X, -tunnel.Nodes[j - 1].Y); Vector2 end = new Vector2(tunnel.Nodes[j].X, -tunnel.Nodes[j].Y); GUI.DrawLine(spriteBatch, start, end, tunnelColor, width: (int)(2.0f / Cam.Zoom)); } } foreach (Level.InterestingPosition interestingPos in Level.Loaded.PositionsOfInterest) { if (interestingPos.Position.X < Cam.WorldView.X || interestingPos.Position.X > Cam.WorldView.Right || interestingPos.Position.Y > Cam.WorldView.Y || interestingPos.Position.Y < Cam.WorldView.Y - Cam.WorldView.Height) { continue; } Vector2 pos = new Vector2(interestingPos.Position.X, -interestingPos.Position.Y); spriteBatch.DrawCircle(pos, 500, 6, Color.White * 0.5f, thickness: (int)(2 / Cam.Zoom)); GUI.DrawString(spriteBatch, pos, interestingPos.PositionType.ToString(), Color.White, font: GUIStyle.LargeFont); } // TODO: Improve this temporary level editor debug solution foreach (var pathPoint in Level.Loaded.PathPoints) { Vector2 pathPointPos = new Vector2(pathPoint.Position.X, -pathPoint.Position.Y); foreach (var location in pathPoint.ClusterLocations) { if (location.Resources == null) { continue; } foreach (var resource in location.Resources) { Vector2 resourcePos = new Vector2(resource.Position.X, -resource.Position.Y); spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / Cam.Zoom)); GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUIStyle.LargeFont); var dist = Vector2.Distance(resourcePos, pathPointPos); var lineStartPos = Vector2.Lerp(resourcePos, pathPointPos, 110 / dist); var lineEndPos = Vector2.Lerp(pathPointPos, resourcePos, 310 / dist); GUI.DrawLine(spriteBatch, lineStartPos, lineEndPos, Color.DarkGreen * 0.5f, width: (int)(2 / Cam.Zoom)); } } var color = pathPoint.ShouldContainResources ? Color.DarkGreen : Color.DarkRed; spriteBatch.DrawCircle(pathPointPos, 300, 6, color * 0.5f, thickness: (int)(2 / Cam.Zoom)); GUI.DrawString(spriteBatch, pathPointPos, "Path Point\n" + pathPoint.Id, color, font: GUIStyle.LargeFont); } foreach (var location in Level.Loaded.AbyssResources) { if (location.Resources == null) { continue; } foreach (var resource in location.Resources) { Vector2 resourcePos = new Vector2(resource.Position.X, -resource.Position.Y); spriteBatch.DrawCircle(resourcePos, 100, 6, Color.DarkGreen * 0.5f, thickness: (int)(2 / Cam.Zoom)); GUI.DrawString(spriteBatch, resourcePos, resource.Name, Color.DarkGreen, font: GUIStyle.LargeFont); } } /*for (int i = 0; i < Level.Loaded.distanceField.Count; i++) { GUI.DrawRectangle(spriteBatch, new Vector2(Level.Loaded.distanceField[i].First.X, -Level.Loaded.distanceField[i].First.Y), Vector2.One * 5 / cam.Zoom, ToolBox.GradientLerp((float)(Level.Loaded.distanceField[i].Second / 20000.0), Color.Red, Color.Orange, Color.Yellow, Color.LightGreen), isFilled: true); }*/ /*for (int i = 0; i < Level.Loaded.siteCoordsX.Count; i++) { GUI.DrawRectangle(spriteBatch, new Vector2((float)Level.Loaded.siteCoordsX[i], -(float)Level.Loaded.siteCoordsY[i]), Vector2.One * 5 / cam.Zoom, Color.Red, isFilled: true); }*/ spriteBatch.End(); if (lightingEnabled.Selected) { spriteBatch.Begin(SpriteSortMode.Immediate, Lights.CustomBlendStates.Multiplicative, null, DepthStencilState.None, null, null, null); spriteBatch.Draw(GameMain.LightManager.LightMap, new Rectangle(0, 0, GameMain.GraphicsWidth, GameMain.GraphicsHeight), Color.White); spriteBatch.End(); } } if (editingSprite != null) { GameMain.SpriteEditorScreen.Draw(deltaTime, graphics, spriteBatch); } spriteBatch.Begin(SpriteSortMode.Deferred, samplerState: GUI.SamplerState, rasterizerState: GameMain.ScissorTestEnable); if (Level.Loaded != null) { float hullUpgradePrcIncrease = UpgradePrefab.CrushDepthUpgradePrc / 100f; for (int upgradeLevel = 0; upgradeLevel <= UpgradePrefab.IncreaseWallHealthMaxLevel; upgradeLevel++) { float upgradeLevelCrushDepth = Level.DefaultRealWorldCrushDepth + (Level.DefaultRealWorldCrushDepth * upgradeLevel * hullUpgradePrcIncrease); float subCrushDepth = (upgradeLevelCrushDepth / Physics.DisplayToRealWorldRatio) - Level.Loaded.LevelData.InitialDepth; string labelText = $"Crush depth (upgrade level {upgradeLevel})"; if (upgradeLevel == 0) { labelText = $"Crush depth (no upgrade)"; } DrawCrushDepth(subCrushDepth, labelText, Color.Red); } float abyssStartScreen = Cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Bottom)).Y; if (abyssStartScreen > 0.0f && abyssStartScreen < GameMain.GraphicsHeight) { GUI.DrawLine(spriteBatch, new Vector2(0, abyssStartScreen), new Vector2(GameMain.GraphicsWidth, abyssStartScreen), GUIStyle.Blue * 0.25f, width: 5); GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssStartScreen), "Abyss start", GUIStyle.Blue, backgroundColor: Color.Black); } float abyssEndScreen = Cam.WorldToScreen(new Vector2(0.0f, Level.Loaded.AbyssArea.Y)).Y; if (abyssEndScreen > 0.0f && abyssEndScreen < GameMain.GraphicsHeight) { GUI.DrawLine(spriteBatch, new Vector2(0, abyssEndScreen), new Vector2(GameMain.GraphicsWidth, abyssEndScreen), GUIStyle.Blue * 0.25f, width: 5); GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, abyssEndScreen), "Abyss end", GUIStyle.Blue, backgroundColor: Color.Black); } } GUI.Draw(Cam, spriteBatch); spriteBatch.End(); void DrawCrushDepth(float crushDepth, string labelText, Color color) { float crushDepthScreen = Cam.WorldToScreen(new Vector2(0.0f, -crushDepth)).Y; if (crushDepthScreen > 0.0f && crushDepthScreen < GameMain.GraphicsHeight) { GUI.DrawLine(spriteBatch, new Vector2(0, crushDepthScreen), new Vector2(GameMain.GraphicsWidth, crushDepthScreen), color * 0.25f, width: 5); GUI.DrawString(spriteBatch, new Vector2(GameMain.GraphicsWidth / 2, crushDepthScreen), labelText, GUIStyle.Red, backgroundColor: Color.Black); } } } public override void Update(double deltaTime) { if (GameMain.GraphicsWidth != prevResolution.X || GameMain.GraphicsHeight != prevResolution.Y) { RefreshUI(forceCreate: true); } if (lightingEnabled.Selected) { foreach (Item item in Item.ItemList) { if (item == null || item.IsHidden) { continue; } foreach (var light in item.GetComponents()) { light.Update((float)deltaTime, Cam); } } } GameMain.LightManager?.Update((float)deltaTime); pointerLightSource.Position = Cam.ScreenToWorld(PlayerInput.MousePosition); pointerLightSource.Enabled = cursorLightEnabled.Selected; pointerLightSource.IsBackground = true; Cam.MoveCamera((float)deltaTime, allowZoom: GUI.MouseOn == null); Cam.UpdateTransform(); Level.Loaded?.Update((float)deltaTime, Cam); if (editingSprite != null) { GameMain.SpriteEditorScreen.Update(deltaTime); } // in case forced difficulty was changed by console command or such if (Level.ForcedDifficulty != null && MathHelper.Distance((float)Level.ForcedDifficulty, forceDifficultyInput.FloatValue) > 0.001f) { forceDifficultyInput.FloatValue = (float)Level.ForcedDifficulty; } #if DEBUG if (PlayerInput.KeyDown(Keys.LeftControl) && PlayerInput.KeyHit(Keys.R)) { RefreshUI(forceCreate: true); } #endif } private void SerializeAll() { IEnumerable packages = ContentPackageManager.LocalPackages; #if DEBUG packages = packages.Union(ContentPackageManager.VanillaCorePackage.ToEnumerable()); #endif System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true }; foreach (var configFile in packages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } foreach (LevelGenerationParams genParams in LevelGenerationParams.LevelParams) { foreach (XElement element in doc.Root.Elements()) { if (element.IsOverride()) { foreach (var subElement in element.Elements()) { string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(genParams, element, true); } } else { string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(genParams, element, true); } break; } } using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); } } foreach (var configFile in packages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } foreach (CaveGenerationParams genParams in CaveGenerationParams.CaveParams) { foreach (XElement element in doc.Root.Elements()) { if (element.IsOverride()) { foreach (var subElement in element.Elements()) { string id = subElement.GetAttributeString("identifier", null) ?? subElement.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } genParams.Save(subElement); } } else { string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } genParams.Save(element); } break; } } using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); } } settings.NewLineOnAttributes = false; foreach (var configFile in packages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } foreach (LevelObjectPrefab levelObjPrefab in LevelObjectPrefab.Prefabs) { foreach (XElement element in doc.Root.Elements()) { Identifier identifier = element.GetAttributeIdentifier("identifier", ""); if (identifier != levelObjPrefab.Identifier) { continue; } levelObjPrefab.Save(element); break; } } using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); } } RuinGenerationParams.SaveAll(); } private void Serialize(LevelGenerationParams genParams) { foreach (var configFile in ContentPackageManager.AllPackages.SelectMany(p => p.GetFiles())) { XDocument doc = XMLExtensions.TryLoadXml(configFile.Path); if (doc == null) { continue; } bool elementFound = false; foreach (XElement element in doc.Root.Elements()) { string id = element.GetAttributeString("identifier", null) ?? element.Name.ToString(); if (!id.Equals(genParams.Name, StringComparison.OrdinalIgnoreCase)) { continue; } SerializableProperty.SerializeProperties(genParams, element, true); elementFound = true; } if (elementFound) { System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true }; using (var writer = XmlWriter.Create(configFile.Path.Value, settings)) { doc.WriteTo(writer); writer.Flush(); } return; } } } #region LevelObject Wizard private class Wizard { private LevelObjectPrefab newPrefab; private static Wizard instance; public static Wizard Instance { get { if (instance == null) { instance = new Wizard(); } return instance; } } public void AddToGUIUpdateList() { //activeView?.Box.AddToGUIUpdateList(); } public GUIMessageBox Create() { var box = new GUIMessageBox(TextManager.Get("leveleditor.createlevelobj"), string.Empty, new LocalizedString[] { TextManager.Get("cancel"), TextManager.Get("done") }, new Vector2(0.5f, 0.8f)); box.Content.ChildAnchor = Anchor.TopCenter; box.Content.AbsoluteSpacing = 20; int elementSize = 30; var listBox = new GUIListBox(new RectTransform(new Vector2(1, 0.75f), box.Content.RectTransform)); new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform), TextManager.Get("leveleditor.levelobjname")) { CanBeFocused = false }; var nameBox = new GUITextBox(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform)); new GUITextBlock(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform), TextManager.Get("leveleditor.levelobjtexturepath")) { CanBeFocused = false }; var texturePathBox = new GUITextBox(new RectTransform(new Point(listBox.Content.Rect.Width, elementSize), listBox.Content.RectTransform)); foreach (LevelObjectPrefab prefab in LevelObjectPrefab.Prefabs) { if (prefab.Sprites.FirstOrDefault() == null) { continue; } texturePathBox.Text = Path.GetDirectoryName(prefab.Sprites.FirstOrDefault().FilePath.Value); break; } //this is nasty :( newPrefab = new LevelObjectPrefab(null, null, new Identifier("No identifier")); new SerializableEntityEditor(listBox.Content.RectTransform, newPrefab, false, false); box.Buttons[0].OnClicked += (b, d) => { box.Close(); return true; }; // Next box.Buttons[1].OnClicked += (b, d) => { if (string.IsNullOrEmpty(nameBox.Text)) { nameBox.Flash(GUIStyle.Red); GUI.AddMessage(TextManager.Get("leveleditor.levelobjnameempty"), GUIStyle.Red); return false; } if (LevelObjectPrefab.Prefabs.Any(obj => obj.Identifier == nameBox.Text)) { nameBox.Flash(GUIStyle.Red); GUI.AddMessage(TextManager.Get("leveleditor.levelobjnametaken"), GUIStyle.Red); return false; } if (!File.Exists(texturePathBox.Text)) { texturePathBox.Flash(GUIStyle.Red); GUI.AddMessage(TextManager.Get("leveleditor.levelobjtexturenotfound"), GUIStyle.Red); return false; } System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true }; var newElement = new XElement(nameBox.Text); newPrefab.Save(newElement); newElement.Add(new XElement("Sprite", new XAttribute("texture", texturePathBox.Text), new XAttribute("sourcerect", "0,0,100,100"), new XAttribute("origin", "0.5,0.5"))); // Create a new mod for the purpose of providing this new prefab #warning TODO: add a clear way to tack it into an existing content package? string modDir = Path.Combine(ContentPackage.LocalModsDir, nameBox.Text); Directory.CreateDirectory(modDir); string fileListPath = Path.Combine(modDir, ContentPackage.FileListFileName); string prefabFilePath = Path.Combine(modDir, $"{nameBox.Text}.xml"); var newMod = new ModProject { Name = nameBox.Text }; var newFile = ModProject.File.FromPath(prefabFilePath); newMod.AddFile(newFile); XDocument fileListDoc = newMod.ToXDocument(); Directory.CreateDirectory(Path.GetDirectoryName(fileListPath)); using (XmlWriter writer = XmlWriter.Create(fileListPath, settings)) { fileListDoc.Save(writer); } XDocument prefabDoc = new XDocument(); var prefabFileRoot = new XElement("LevelObjects"); prefabFileRoot.Add(newElement); prefabDoc.Add(prefabFileRoot); using (XmlWriter writer = XmlWriter.Create(prefabFilePath, settings)) { prefabDoc.Save(writer); } ContentPackageManager.UpdateContentPackageList(); var newRegularList = ContentPackageManager.EnabledPackages.Regular.ToList(); newRegularList.Add(ContentPackageManager.RegularPackages.First(p => p.Name == nameBox.Text)); ContentPackageManager.EnabledPackages.SetRegular(newRegularList); GameMain.LevelEditorScreen.UpdateLevelObjectsList(); box.Close(); return true; }; return box; } } #endregion } }