Przeglądaj źródła

Add scene management

habeebweeb 4 lat temu
rodzic
commit
e8b73bc994

+ 21 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Constants.cs

@@ -55,6 +55,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             = new Dictionary<string, List<MyRoomItem>>();
         public static readonly Dictionary<string, List<ModItem>> ModPropDict
             = new Dictionary<string, List<ModItem>>(StringComparer.InvariantCultureIgnoreCase);
+        public static readonly List<string> SceneDirectoryList = new List<string>();
+        public static readonly List<string> KankyoDirectoryList = new List<string>();
         public static int CustomPoseGroupsIndex { get; private set; } = -1;
         public static int MyRoomCustomBGIndex { get; private set; } = -1;
         public static bool HandItemsInitialized { get; private set; } = false;
@@ -96,6 +98,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         public static void Initialize()
         {
+            InitializeScenes();
             InitializePoses();
             InitializeHandPresets();
             InitializeFaceBlends();
@@ -209,6 +212,24 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             customHandChange?.Invoke(null, new CustomPoseEventArgs(fullPath, category));
         }
 
+        public static void InitializeScenes()
+        {
+            SceneDirectoryList.Clear();
+            KankyoDirectoryList.Clear();
+
+            SceneDirectoryList.Add(sceneDirectory);
+            foreach (string directory in Directory.GetDirectories(scenesPath))
+            {
+                SceneDirectoryList.Add(new DirectoryInfo(directory).Name);
+            }
+
+            KankyoDirectoryList.Add(kankyoDirectory);
+            foreach (string directory in Directory.GetDirectories(kankyoPath))
+            {
+                KankyoDirectoryList.Add(new DirectoryInfo(directory).Name);
+            }
+        }
+
         public static void InitializePoses()
         {
             // Load Poses

+ 3 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/BasePane.cs

@@ -5,6 +5,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 {
     internal abstract class BasePane
     {
+        protected BaseWindow parent;
         protected List<BaseControl> Controls { get; set; }
         protected bool updating = false;
         public virtual bool Visible { get; set; }
@@ -26,6 +27,8 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             ReloadTranslation();
         }
 
+        public void SetParent(BaseWindow window) => this.parent = window;
+
         protected virtual void ReloadTranslation() { }
 
         public virtual void UpdatePane() { }

+ 7 - 1
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/MainWindowPanes/BGWindowPane.cs

@@ -8,11 +8,16 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         private LightsPane lightsPane;
         private EffectsPane effectsPane;
         private DragPointPane dragPointPane;
+        private Button sceneManagerButton;
 
         public BGWindowPane(
-            EnvironmentManager environmentManager, LightManager lightManager, EffectManager effectManager
+            EnvironmentManager environmentManager, LightManager lightManager, EffectManager effectManager,
+            SceneWindow sceneWindow
         )
         {
+            this.sceneManagerButton = new Button("Manage Scenes");
+            this.sceneManagerButton.ControlEvent += (s, a) => sceneWindow.Visible = !sceneWindow.Visible;
+
             this.backgroundSelectorPane = AddPane(new BackgroundSelectorPane(environmentManager));
             this.dragPointPane = AddPane(new DragPointPane());
             this.lightsPane = AddPane(new LightsPane(lightManager));
@@ -28,6 +33,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         public override void Draw()
         {
+            this.sceneManagerButton.Draw();
             this.backgroundSelectorPane.Draw();
             this.dragPointPane.Draw();
             this.scrollPos = GUILayout.BeginScrollView(this.scrollPos);

+ 93 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/SceneManagerPanes/SceneManagerDirectoryPane.cs

@@ -0,0 +1,93 @@
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    internal class SceneManagerDirectoryPane : BasePane
+    {
+        public static readonly int listWidth = 200;
+        private SceneManager sceneManager;
+        private SceneModalWindow sceneModalWindow;
+        private Button createDirectoryButton;
+        private Button deleteDirectoryButton;
+        private TextField directoryTextField;
+        private Button cancelButton;
+        private Vector2 listScrollPos;
+        private bool createDirectoryMode;
+        private Texture2D selectedTexture = Utility.MakeTex(2, 2, new Color(0.5f, 0.5f, 0.5f, 0.4f));
+
+        public SceneManagerDirectoryPane(SceneManager sceneManager, SceneModalWindow sceneModalWindow)
+        {
+            this.sceneManager = sceneManager;
+            this.sceneModalWindow = sceneModalWindow;
+
+            this.createDirectoryButton = new Button("New Folder");
+            this.createDirectoryButton.ControlEvent += (s, a) => createDirectoryMode = true;
+
+            this.deleteDirectoryButton = new Button("Delete");
+            this.deleteDirectoryButton.ControlEvent += (s, a) => sceneModalWindow.ShowDirectoryDialogue();
+
+            this.directoryTextField = new TextField();
+            this.directoryTextField.ControlEvent += (s, a) =>
+            {
+                sceneManager.AddDirectory(directoryTextField.Value);
+                createDirectoryMode = false;
+                directoryTextField.Value = string.Empty;
+            };
+
+            this.cancelButton = new Button("X");
+            this.cancelButton.ControlEvent += (s, a) => createDirectoryMode = false;
+        }
+
+        public override void Draw()
+        {
+            GUIStyle directoryStyle = new GUIStyle(GUI.skin.button);
+            directoryStyle.fontSize = Utility.GetPix(12);
+            directoryStyle.alignment = TextAnchor.MiddleLeft;
+            directoryStyle.margin = new RectOffset(0, 0, 0, 0);
+
+            GUIStyle directorySelectedStyle = new GUIStyle(directoryStyle);
+            directorySelectedStyle.normal.textColor = Color.white;
+            directorySelectedStyle.normal.background = selectedTexture;
+            directorySelectedStyle.hover.background = selectedTexture;
+
+            GUILayout.BeginVertical(GUILayout.Width(Utility.GetPix(listWidth)));
+
+            listScrollPos = GUILayout.BeginScrollView(listScrollPos);
+
+            for (int i = 0; i < sceneManager.CurrentDirectoryList.Count; i++)
+            {
+                GUIStyle style = i == sceneManager.CurrentDirectoryIndex ? directorySelectedStyle : directoryStyle;
+                string directoryName = sceneManager.CurrentDirectoryList[i];
+                if (GUILayout.Button(directoryName, style, GUILayout.Height(Utility.GetPix(20))))
+                {
+                    sceneManager.SelectDirectory(i);
+                }
+            }
+
+            GUILayout.EndScrollView();
+
+            GUILayout.BeginHorizontal();
+
+            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
+            buttonStyle.fontSize = Utility.GetPix(12);
+
+            GUILayoutOption buttonHeight = GUILayout.Height(Utility.GetPix(20));
+
+            if (createDirectoryMode)
+            {
+                directoryTextField.Draw(buttonHeight, GUILayout.Width(Utility.GetPix(listWidth - 30)));
+                cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+            }
+            else
+            {
+                createDirectoryButton.Draw(buttonStyle, buttonHeight);
+                GUI.enabled = sceneManager.CurrentDirectoryIndex > 0;
+                deleteDirectoryButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+                GUI.enabled = true;
+            }
+            GUILayout.EndHorizontal();
+
+            GUILayout.EndVertical();
+        }
+    }
+}

+ 82 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/SceneManagerPanes/SceneManagerScenePane.cs

@@ -0,0 +1,82 @@
+using UnityEngine;
+
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    internal class SceneManagerScenePane : BasePane
+    {
+        public static readonly float thumbnailScale = 0.55f;
+        private SceneManager sceneManager;
+        private SceneModalWindow sceneModalWindow;
+        private Button addSceneButton;
+        private Vector2 sceneScrollPos;
+
+        public SceneManagerScenePane(SceneManager sceneManager, SceneModalWindow sceneModalWindow)
+        {
+            this.sceneManager = sceneManager;
+            this.sceneModalWindow = sceneModalWindow;
+
+            this.addSceneButton = new Button("+");
+            this.addSceneButton.ControlEvent += (s, a) => sceneManager.SaveScene(overwrite: false);
+        }
+
+        public override void Draw()
+        {
+            GUIStyle sceneImageStyle = new GUIStyle(GUI.skin.label);
+            sceneImageStyle.alignment = TextAnchor.MiddleCenter;
+            sceneImageStyle.padding = new RectOffset(0, 0, 0, 0);
+
+            GUIStyle addSceneStyle = new GUIStyle(GUI.skin.button);
+            addSceneStyle.alignment = TextAnchor.MiddleCenter;
+            addSceneStyle.fontSize = 60;
+
+            GUILayout.BeginVertical();
+
+            float sceneWidth = SceneManager.sceneDimensions.x * thumbnailScale;
+            float sceneHeight = SceneManager.sceneDimensions.y * thumbnailScale;
+            float sceneGridWidth = parent.WindowRect.width - SceneManagerDirectoryPane.listWidth;
+
+            GUILayoutOption[] sceneLayoutOptions = new[] { GUILayout.Height(sceneHeight), GUILayout.Width(sceneWidth) };
+
+            int columns = Mathf.Max(1, (int)(sceneGridWidth / sceneWidth));
+            int rows = (int)Mathf.Ceil((float)sceneManager.SceneList.Count + 1 / (float)columns);
+
+            sceneScrollPos = GUILayout.BeginScrollView(sceneScrollPos);
+
+            GUILayout.BeginHorizontal();
+            GUILayout.FlexibleSpace();
+            GUILayout.BeginVertical();
+
+            int currentScene = -1;
+            for (int i = 0; i < rows; i++)
+            {
+                GUILayout.BeginHorizontal();
+                for (int j = 0; j < columns; j++, currentScene++)
+                {
+                    if (currentScene == -1)
+                    {
+                        addSceneButton.Draw(addSceneStyle, sceneLayoutOptions);
+                    }
+                    else if (currentScene < sceneManager.SceneList.Count)
+                    {
+                        SceneManager.Scene scene = sceneManager.SceneList[currentScene];
+                        if (GUILayout.Button(scene.Thumbnail, sceneImageStyle, sceneLayoutOptions))
+                        {
+                            sceneManager.SelectScene(currentScene);
+                            sceneModalWindow.ShowSceneDialogue();
+                        }
+                    }
+                }
+                GUILayout.EndHorizontal();
+            }
+
+            GUILayout.EndVertical();
+            GUILayout.FlexibleSpace();
+            GUILayout.EndHorizontal();
+
+            GUILayout.EndScrollView();
+
+            GUILayout.EndVertical();
+        }
+    }
+}

+ 95 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Panes/SceneManagerPanes/SceneManagerTitleBar.cs

@@ -0,0 +1,95 @@
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    internal class SceneManagerTitleBarPane : BasePane
+    {
+        public event System.EventHandler closeChange;
+        private SceneManager sceneManager;
+        private Button kankyoToggle;
+        private Button refreshButton;
+        private Dropdown sortDropdown;
+        private Toggle descendingToggle;
+        private Button closeButton;
+        private string sortLabel;
+
+        public SceneManagerTitleBarPane(SceneManager sceneManager)
+        {
+            this.sceneManager = sceneManager;
+            kankyoToggle = new Button("Backgrounds");
+            kankyoToggle.ControlEvent += (s, a) => sceneManager.ToggleKankyoMode();
+
+            refreshButton = new Button("Refresh");
+            refreshButton.ControlEvent += (s, a) => sceneManager.Refresh();
+
+            sortDropdown = new Dropdown(new[] { "Name", "Date Created", "Date Modified" });
+            sortDropdown.SelectionChange += (s, a) =>
+            {
+                SceneManager.SortMode sortMode = (SceneManager.SortMode)sortDropdown.SelectedItemIndex;
+                if (sceneManager.CurrentSortMode == sortMode) return;
+                sceneManager.SortScenes(sortMode);
+            };
+
+            descendingToggle = new Toggle("Descending", sceneManager.SortDescending);
+            descendingToggle.ControlEvent += (s, a) =>
+            {
+                sceneManager.SortDescending = descendingToggle.Value;
+                sceneManager.SortScenes(sceneManager.CurrentSortMode);
+            };
+
+            closeButton = new Button("X");
+            closeButton.ControlEvent += (s, a) => closeChange?.Invoke(this, System.EventArgs.Empty);
+
+            sortLabel = "Sort";
+        }
+
+        public override void Draw()
+        {
+            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
+            buttonStyle.fontSize = Utility.GetPix(12);
+
+            GUILayoutOption buttonHeight = GUILayout.Height(Utility.GetPix(20));
+            GUILayout.BeginHorizontal();
+
+            GUILayout.BeginHorizontal(GUILayout.Width(Utility.GetPix(SceneManagerDirectoryPane.listWidth)));
+
+            Color originalColour = GUI.backgroundColor;
+            if (sceneManager.KankyoMode) GUI.backgroundColor = Color.green;
+            kankyoToggle.Draw(buttonStyle, buttonHeight);
+            GUI.backgroundColor = originalColour;
+
+            GUILayout.FlexibleSpace();
+
+            refreshButton.Draw(buttonStyle, buttonHeight);
+
+            GUILayout.EndHorizontal();
+
+            GUILayout.BeginHorizontal();
+
+            GUILayout.Space(Utility.GetPix(15));
+
+            GUIStyle labelStyle = new GUIStyle(GUI.skin.label);
+            labelStyle.fontSize = buttonStyle.fontSize;
+
+            GUILayout.Label(sortLabel, labelStyle);
+
+            GUIStyle dropdownStyle = new GUIStyle(DropdownHelper.DefaultDropdownStyle);
+            dropdownStyle.fontSize = buttonStyle.fontSize;
+
+            sortDropdown.Draw(buttonStyle, dropdownStyle, buttonHeight, GUILayout.Width(Utility.GetPix(100)));
+
+            GUIStyle toggleStyle = new GUIStyle(GUI.skin.toggle);
+            toggleStyle.fontSize = buttonStyle.fontSize;
+
+            descendingToggle.Draw(toggleStyle);
+
+            GUILayout.FlexibleSpace();
+
+            closeButton.Draw();
+
+            GUILayout.EndHorizontal();
+
+            GUILayout.EndHorizontal();
+        }
+    }
+}

+ 232 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/SceneModalWindow.cs

@@ -0,0 +1,232 @@
+using System;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    internal class SceneModalWindow : BaseWindow
+    {
+        private static Texture2D infoHighlight = Utility.MakeTex(2, 2, new Color(0f, 0f, 0f, 0.8f));
+        private SceneManager sceneManager;
+        public override Rect WindowRect
+        {
+            set
+            {
+                value.width = Mathf.Clamp(Screen.width * 0.3f, 360f, 500f);
+                value.height = directoryMode ? 150f : Mathf.Clamp(Screen.height * 0.4f, 240f, 380f);
+                base.WindowRect = value;
+            }
+        }
+        private bool visible;
+        public override bool Visible
+        {
+            get => visible;
+            set
+            {
+                visible = value;
+                if (value)
+                {
+                    WindowRect = WindowRect;
+                    windowRect.x = MiddlePosition.x;
+                    windowRect.y = MiddlePosition.y;
+                }
+            }
+        }
+
+        private Button okButton;
+        private Button cancelButton;
+        private Button deleteButton;
+        private Button overwriteButton;
+        private string deleteDirectoryMessage
+            = "Are you sure you want to permanently delete '{0}' and all of its files?";
+        private string deleteSceneMessage = "Are you sure you want to permanently delete '{0}'?";
+        private string directoryDeleteCommit = "Delete Folder";
+        private string sceneDeleteCommit = "Delete Scene";
+        private string sceneLoadCommit = "Load Scene";
+        private bool directoryMode = false;
+        private bool deleteScene = false;
+
+        public SceneModalWindow(SceneManager sceneManager)
+        {
+            this.sceneManager = sceneManager;
+
+            windowRect.x = MiddlePosition.x;
+            windowRect.y = MiddlePosition.y;
+            this.okButton = new Button(sceneLoadCommit);
+            this.okButton.ControlEvent += (s, a) => Commit();
+
+            this.cancelButton = new Button("Cancel");
+            this.cancelButton.ControlEvent += (s, a) => Cancel();
+
+            this.deleteButton = new Button("Delete");
+            this.deleteButton.ControlEvent += (s, a) =>
+            {
+                okButton.Label = sceneDeleteCommit;
+                deleteScene = true;
+            };
+
+            this.overwriteButton = new Button("Overwrite");
+            this.overwriteButton.ControlEvent += (s, a) =>
+            {
+                sceneManager.OverwriteScene();
+                Visible = false;
+            };
+        }
+
+        public override void Draw()
+        {
+            GUILayout.BeginArea(new Rect(10f, 10f, WindowRect.width - 20f, WindowRect.height - 20f));
+
+            // thumbnail
+            if (!directoryMode)
+            {
+                SceneManager.Scene scene = sceneManager.CurrentScene;
+                Texture2D thumb = scene.Thumbnail;
+
+                float scale = Mathf.Min(
+                    (WindowRect.width - 20f) / (float)thumb.width, (WindowRect.height - 110f) / (float)thumb.height
+                );
+                float width = Mathf.Min(thumb.width, (float)thumb.width * scale);
+                float height = Mathf.Min(thumb.height, (float)thumb.height * scale);
+
+                GUILayout.BeginHorizontal();
+                GUILayout.FlexibleSpace();
+
+                MiscGUI.DrawTexture(thumb, GUILayout.Width(width), GUILayout.Height(height));
+
+                GUIStyle labelStyle = new GUIStyle(GUI.skin.label);
+                labelStyle.fontSize = Utility.GetPix(12);
+                labelStyle.alignment = TextAnchor.MiddleCenter;
+                labelStyle.normal.background = infoHighlight;
+
+                Rect labelBox = GUILayoutUtility.GetLastRect();
+
+                if (scene.NumberOfMaids != SceneManager.Scene.initialNumberOfMaids)
+                {
+                    string infoString = scene.NumberOfMaids == MeidoPhotoStudio.kankyoMagic
+                        ? "Kankyo"
+                        : $"{scene.NumberOfMaids} Maids";
+
+                    Vector2 labelSize = labelStyle.CalcSize(new GUIContent(infoString));
+
+                    labelBox = new Rect(
+                        labelBox.x + 10, labelBox.y + labelBox.height - (labelSize.y + 10),
+                        labelSize.x + 10, labelSize.y + 2
+                    );
+
+                    GUI.Label(labelBox, infoString, labelStyle);
+                }
+
+                GUILayout.FlexibleSpace();
+                GUILayout.EndHorizontal();
+            }
+
+            // message
+            string currentMessage = string.Empty;
+            string context = string.Empty;
+
+            if (directoryMode)
+            {
+                currentMessage = deleteDirectoryMessage;
+                context = sceneManager.CurrentDirectoryName;
+            }
+            else
+            {
+                if (deleteScene)
+                {
+                    currentMessage = deleteSceneMessage;
+                    context = sceneManager.CurrentScene.FileInfo.Name;
+                }
+                else
+                {
+                    currentMessage = sceneManager.CurrentScene.FileInfo.Name;
+                    context = currentMessage;
+                }
+            }
+
+            GUIStyle messageStyle = new GUIStyle(GUI.skin.label);
+            messageStyle.alignment = TextAnchor.MiddleCenter;
+            messageStyle.fontSize = Utility.GetPix(12);
+
+            GUILayout.FlexibleSpace();
+
+            GUILayout.Label(string.Format(currentMessage, context), messageStyle);
+
+            GUILayout.FlexibleSpace();
+
+            // Buttons
+
+            GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
+            buttonStyle.fontSize = Utility.GetPix(12);
+
+            GUILayoutOption buttonHeight = GUILayout.Height(Utility.GetPix(20));
+
+            GUILayout.BeginHorizontal();
+
+            if (directoryMode || deleteScene)
+            {
+                GUILayout.FlexibleSpace();
+                okButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+                cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.Width(100));
+            }
+            else
+            {
+                deleteButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+                overwriteButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+
+                GUILayout.FlexibleSpace();
+
+                okButton.Draw(buttonStyle, buttonHeight, GUILayout.ExpandWidth(false));
+                cancelButton.Draw(buttonStyle, buttonHeight, GUILayout.Width(100));
+            }
+
+            GUILayout.EndHorizontal();
+
+            GUILayout.EndArea();
+        }
+
+        public void ShowDirectoryDialogue()
+        {
+            okButton.Label = directoryDeleteCommit;
+            directoryMode = true;
+            Modal.Show(this);
+        }
+
+        public void ShowSceneDialogue()
+        {
+            directoryMode = false;
+            okButton.Label = sceneLoadCommit;
+            Modal.Show(this);
+        }
+
+        private void Commit()
+        {
+            if (directoryMode)
+            {
+                sceneManager.DeleteDirectory();
+                Modal.Close();
+            }
+            else
+            {
+                if (deleteScene)
+                {
+                    sceneManager.DeleteScene();
+                    deleteScene = false;
+                }
+                else sceneManager.LoadScene();
+
+                Modal.Close();
+            }
+        }
+
+        private void Cancel()
+        {
+            if (directoryMode) Modal.Close();
+            else
+            {
+                if (deleteScene) deleteScene = false;
+                else Modal.Close();
+            }
+            okButton.Label = sceneLoadCommit;
+        }
+    }
+}

+ 111 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/GUI/Windows/SceneWindow.cs

@@ -0,0 +1,111 @@
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    internal class SceneWindow : BaseWindow
+    {
+        private SceneManager sceneManager;
+        private SceneManagerTitleBarPane titleBar;
+        private SceneManagerDirectoryPane directoryList;
+        private SceneManagerScenePane sceneGrid;
+        private Rect resizeHandleRect;
+        private bool resizing;
+        private float resizeHandleSize = 15f;
+        private bool visible;
+        public override bool Visible
+        {
+            get => visible;
+            set
+            {
+                visible = value;
+                if (visible && !sceneManager.Initialized) sceneManager.Initialize();
+            }
+        }
+
+        public SceneWindow(SceneManager sceneManager)
+        {
+            windowRect.width = Screen.width * 0.65f;
+            windowRect.height = Screen.height * 0.75f;
+            windowRect.x = Screen.width * 0.5f - windowRect.width * 0.5f;
+            windowRect.y = Screen.height * 0.5f - windowRect.height * 0.5f;
+
+            resizeHandleRect = new Rect(0f, 0f, resizeHandleSize, resizeHandleSize);
+
+
+            this.sceneManager = sceneManager;
+            SceneModalWindow sceneModalWindow = new SceneModalWindow(this.sceneManager);
+
+            this.titleBar = AddPane(new SceneManagerTitleBarPane(sceneManager));
+            this.titleBar.closeChange += (s, a) => Visible = false;
+
+            this.directoryList = AddPane(new SceneManagerDirectoryPane(sceneManager, sceneModalWindow));
+
+            this.sceneGrid = AddPane(new SceneManagerScenePane(sceneManager, sceneModalWindow));
+
+            this.sceneGrid.SetParent(this);
+        }
+
+        public override void GUIFunc(int id)
+        {
+            HandleResize();
+            Draw();
+            if (!resizing) GUI.DragWindow();
+        }
+
+        public override void Update()
+        {
+            base.Update();
+            if (Input.GetKeyDown(KeyCode.F8)) Visible = !Visible;
+        }
+
+        public override void Deactivate()
+        {
+            Visible = false;
+        }
+
+        public override void Draw()
+        {
+            GUI.enabled = !SceneManager.Busy && !Modal.Visible;
+
+            GUILayout.BeginArea(new Rect(10f, 10f, windowRect.width - 20f, windowRect.height - 20f));
+
+            titleBar.Draw();
+
+            GUILayout.BeginHorizontal();
+            directoryList.Draw();
+            sceneGrid.Draw();
+            GUILayout.EndHorizontal();
+
+            GUILayout.EndArea();
+
+            GUI.Box(resizeHandleRect, GUIContent.none);
+        }
+
+        private void HandleResize()
+        {
+            resizeHandleRect.x = windowRect.width - resizeHandleSize;
+            resizeHandleRect.y = windowRect.height - resizeHandleSize;
+
+            if (!resizing && Input.GetMouseButton(0) && resizeHandleRect.Contains(Event.current.mousePosition))
+            {
+                resizing = true;
+            }
+
+            if (resizing)
+            {
+                float rectWidth = Event.current.mousePosition.x;
+                float rectHeight = Event.current.mousePosition.y;
+
+                float minWidth = Utility.GetPix(
+                    SceneManagerDirectoryPane.listWidth
+                    + (int)(SceneManager.sceneDimensions.x * SceneManagerScenePane.thumbnailScale)
+                );
+
+                windowRect.width = Mathf.Max(minWidth, rectWidth);
+                windowRect.height = Mathf.Max(300, rectHeight);
+
+                if (!Input.GetMouseButton(0)) resizing = false;
+            }
+        }
+    }
+}

+ 334 - 0
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/Managers/SceneManager.cs

@@ -0,0 +1,334 @@
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Linq;
+using UnityEngine;
+
+namespace COM3D2.MeidoPhotoStudio.Plugin
+{
+    internal class SceneManager : IManager
+    {
+        public static bool Busy { get; private set; } = false;
+        public bool Initialized { get; private set; } = false;
+        private MeidoPhotoStudio meidoPhotoStudio;
+        private SceneModalWindow sceneModal;
+        private int SortDirection => SortDescending ? -1 : 1;
+        public static Vector2 sceneDimensions = new Vector2(480, 270);
+        public bool KankyoMode { get; set; } = false;
+        public bool SortDescending { get; set; } = false;
+        public List<Scene> SceneList { get; private set; } = new List<Scene>();
+        public int CurrentDirectoryIndex { get; private set; } = -1;
+        public string CurrentDirectoryName => CurrentDirectoryList[CurrentDirectoryIndex];
+        public List<string> CurrentDirectoryList
+        {
+            get => KankyoMode ? Constants.KankyoDirectoryList : Constants.SceneDirectoryList;
+        }
+        public string CurrentBasePath => KankyoMode ? Constants.kankyoPath : Constants.scenesPath;
+        public string CurrentScenesDirectory
+        {
+            get => CurrentDirectoryIndex == 0 ? CurrentBasePath : Path.Combine(CurrentBasePath, CurrentDirectoryName);
+        }
+        public SortMode CurrentSortMode { get; private set; } = SortMode.Name;
+        public int CurrentSceneIndex { get; private set; } = -1;
+        public Scene CurrentScene
+        {
+            get
+            {
+                if (SceneList.Count == 0) return null;
+                return SceneList[CurrentSceneIndex];
+            }
+        }
+        public enum SortMode
+        {
+            Name, DateCreated, DateModified
+        }
+
+        public SceneManager(MeidoPhotoStudio meidoPhotoStudio)
+        {
+            this.meidoPhotoStudio = meidoPhotoStudio;
+            this.sceneModal = new SceneModalWindow(this);
+        }
+
+        public void Activate() { }
+
+        public void Initialize()
+        {
+            if (!Initialized)
+            {
+                Initialized = true;
+                SelectDirectory(0);
+            }
+        }
+
+        public void Deactivate() => ClearSceneList();
+
+        public void Update()
+        {
+            if (Utility.GetModKey(Utility.ModKey.Control))
+            {
+                if (Input.GetKeyDown(KeyCode.S)) QuickSaveScene();
+                else if (Input.GetKeyDown(KeyCode.A)) QuickLoadScene();
+            }
+        }
+
+        public void DeleteDirectory()
+        {
+            if (Directory.Exists(CurrentScenesDirectory))
+            {
+                Directory.Delete(CurrentScenesDirectory, true);
+            }
+            CurrentDirectoryList.RemoveAt(CurrentDirectoryIndex);
+        }
+
+        public void OverwriteScene() => SaveScene(overwrite: true);
+
+        public void ToggleKankyoMode()
+        {
+            this.KankyoMode = !this.KankyoMode;
+            CurrentDirectoryIndex = 0;
+            UpdateSceneList();
+        }
+
+        public void SaveScene(bool overwrite = false)
+        {
+            if (Busy) return;
+            if (!Directory.Exists(CurrentScenesDirectory)) Directory.CreateDirectory(CurrentScenesDirectory);
+            meidoPhotoStudio.StartCoroutine(SaveSceneToFile(overwrite));
+        }
+
+        public void SelectDirectory(int directoryIndex)
+        {
+            if (directoryIndex == CurrentDirectoryIndex) return;
+
+            CurrentDirectoryIndex = directoryIndex;
+
+            UpdateSceneList();
+        }
+
+        public void SelectScene(int sceneIndex)
+        {
+            CurrentSceneIndex = sceneIndex;
+            CurrentScene.GetNumberOfMaids();
+        }
+
+        public void AddDirectory(string directoryName)
+        {
+            if (!CurrentDirectoryList.Contains(directoryName, StringComparer.InvariantCultureIgnoreCase))
+            {
+                CurrentDirectoryList.Add(directoryName);
+                Directory.CreateDirectory(Path.Combine(CurrentBasePath, directoryName));
+                UpdateDirectoryList();
+            }
+        }
+
+        public void Refresh()
+        {
+            Constants.InitializeScenes();
+            UpdateSceneList();
+        }
+
+        public void SortScenes(SortMode sortMode)
+        {
+            CurrentSortMode = sortMode;
+            Comparison<Scene> comparator;
+            switch (CurrentSortMode)
+            {
+                case SortMode.DateModified: comparator = SortByDateModified; break;
+                case SortMode.DateCreated: comparator = SortByDateCreated; break;
+                default: comparator = SortByName; break;
+            }
+            SceneList.Sort(comparator);
+        }
+
+        public void DeleteScene()
+        {
+            if (CurrentScene.FileInfo.Exists)
+            {
+                CurrentScene.FileInfo.Delete();
+            }
+            SceneList.RemoveAt(CurrentSceneIndex);
+        }
+
+        public void LoadScene()
+        {
+            meidoPhotoStudio.ApplyScene(CurrentScene.FileInfo.FullName);
+        }
+
+        private int SortByName(Scene a, Scene b)
+        {
+            return SortDirection * string.Compare(a.FileInfo.Name, b.FileInfo.Name);
+        }
+
+        private int SortByDateCreated(Scene a, Scene b)
+        {
+            return SortDirection * DateTime.Compare(a.FileInfo.CreationTime, b.FileInfo.CreationTime);
+        }
+
+        private int SortByDateModified(Scene a, Scene b)
+        {
+            return SortDirection * DateTime.Compare(a.FileInfo.LastWriteTime, b.FileInfo.LastWriteTime);
+        }
+
+        private void UpdateSceneList()
+        {
+            ClearSceneList();
+
+            if (!Directory.Exists(CurrentScenesDirectory))
+            {
+                Directory.CreateDirectory(CurrentScenesDirectory);
+            }
+
+            foreach (string filename in Directory.GetFiles(CurrentScenesDirectory))
+            {
+                if (Path.GetExtension(filename) == ".png") SceneList.Add(new Scene(filename));
+            }
+
+            SortScenes(CurrentSortMode);
+        }
+
+        private void UpdateDirectoryList()
+        {
+            string baseDirectoryName = KankyoMode ? Constants.kankyoDirectory : Constants.sceneDirectory;
+            CurrentDirectoryList.Sort((a, b) =>
+            {
+                if (a.Equals(baseDirectoryName, StringComparison.InvariantCultureIgnoreCase)) return -1;
+                else return a.CompareTo(b);
+            });
+        }
+
+        private void ClearSceneList()
+        {
+            foreach (Scene scene in SceneList) scene.Destroy();
+            SceneList.Clear();
+        }
+
+        private void QuickSaveScene()
+        {
+            if (Busy) return;
+            byte[] data = meidoPhotoStudio.SerializeScene(kankyo: false);
+            if (data == null) return;
+            File.WriteAllBytes(Path.Combine(Constants.configPath, "mpstempscene"), data);
+        }
+
+        private void QuickLoadScene()
+        {
+            if (Busy) return;
+            meidoPhotoStudio.ApplyScene(Path.Combine(Constants.configPath, "mpstempscene"));
+        }
+
+        private System.Collections.IEnumerator SaveSceneToFile(bool overwrite = false)
+        {
+            Busy = true;
+
+            byte[] sceneData = meidoPhotoStudio.SerializeScene(KankyoMode);
+
+            if (sceneData != null)
+            {
+                string screenshotPath = Utility.TempScreenshotFilename();
+
+                MeidoPhotoStudio.TakeScreenshot(screenshotPath, 1, KankyoMode);
+
+                do yield return new WaitForSecondsRealtime(0.2f);
+                while (!File.Exists(screenshotPath));
+
+                string scenePrefix = KankyoMode ? "mpskankyo" : "mpsscene";
+                string fileName = $"{scenePrefix}{System.DateTime.Now:yyyyMMddHHmmss}.png";
+                string savePath = Path.Combine(CurrentScenesDirectory, fileName);
+
+                if (overwrite && CurrentScene != null)
+                {
+                    savePath = CurrentScene.FileInfo.FullName;
+                }
+                else overwrite = false;
+
+                Texture2D screenshot = new Texture2D(1, 1, TextureFormat.ARGB32, false);
+                screenshot.LoadImage(File.ReadAllBytes(screenshotPath));
+
+                int sceneWidth = (int)SceneManager.sceneDimensions.x;
+                int sceneHeight = (int)SceneManager.sceneDimensions.y;
+                Utility.ResizeToFit(screenshot, sceneWidth, sceneHeight);
+
+                using (FileStream fileStream = File.Create(savePath))
+                {
+                    byte[] encodedPng = screenshot.EncodeToPNG();
+                    fileStream.Write(encodedPng, 0, encodedPng.Length);
+                    fileStream.Write(sceneData, 0, sceneData.Length);
+                }
+
+                UnityEngine.Object.DestroyImmediate(screenshot);
+
+                if (overwrite)
+                {
+                    File.SetCreationTime(savePath, CurrentScene.FileInfo.CreationTime);
+                    CurrentScene.Destroy();
+                    SceneList.RemoveAt(CurrentSceneIndex);
+                }
+
+                SceneList.Add(new Scene(savePath));
+                SortScenes(CurrentSortMode);
+
+            }
+
+            Busy = false;
+        }
+
+        public class Scene
+        {
+            public const int initialNumberOfMaids = -2;
+            public Texture2D Thumbnail { get; private set; }
+            public FileInfo FileInfo { get; set; }
+            public int NumberOfMaids { get; private set; } = initialNumberOfMaids;
+
+            public Scene(string filePath)
+            {
+                FileInfo = new FileInfo(filePath);
+                Thumbnail = new Texture2D(1, 1, TextureFormat.ARGB32, false);
+                Thumbnail.LoadImage(File.ReadAllBytes(FileInfo.FullName));
+            }
+
+            public void GetNumberOfMaids()
+            {
+                if (NumberOfMaids != initialNumberOfMaids) return;
+
+                string filePath = FileInfo.FullName;
+
+                byte[] sceneData = MeidoPhotoStudio.DecompressScene(filePath);
+
+                if (sceneData == null) return;
+
+                using (MemoryStream memoryStream = new MemoryStream(sceneData))
+                using (BinaryReader binaryReader = new BinaryReader(memoryStream, System.Text.Encoding.UTF8))
+                {
+                    try
+                    {
+                        if (binaryReader.ReadString() != "MPS_SCENE")
+                        {
+                            Utility.LogWarning($"'{filePath}' is not a {MeidoPhotoStudio.pluginName} scene");
+                            return;
+                        }
+
+                        if (binaryReader.ReadInt32() > MeidoPhotoStudio.sceneVersion)
+                        {
+                            Utility.LogWarning(
+                                $"'{filePath}' is made in a newer version of {MeidoPhotoStudio.pluginName}"
+                            );
+                            return;
+                        }
+
+                        NumberOfMaids = binaryReader.ReadInt32();
+                    }
+                    catch (Exception e)
+                    {
+                        Utility.LogWarning($"Failed to deserialize scene '{filePath}' because {e.Message}");
+                        return;
+                    }
+                }
+            }
+
+            public void Destroy()
+            {
+                if (Thumbnail != null) UnityEngine.Object.DestroyImmediate(Thumbnail);
+            }
+        }
+    }
+}

+ 13 - 4
COM3D2.MeidoPhotoStudio.Plugin/MeidoPhotoStudio/MeidoPhotoStudio.cs

@@ -1,3 +1,4 @@
+using System;
 using System.IO;
 using System.Collections;
 using System.Collections.Generic;
@@ -20,6 +21,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         public const int kankyoMagic = -765;
         public static string pluginString = $"{pluginName} {pluginVersion}";
         private WindowManager windowManager;
+        private SceneManager sceneManager;
         private MeidoManager meidoManager;
         private EnvironmentManager environmentManager;
         private MessageWindowManager messageWindowManager;
@@ -41,7 +43,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
         {
             Constants.Initialize();
             Translation.Initialize(Configuration.CurrentLanguage);
-            SceneManager.sceneLoaded += OnSceneLoaded;
+            UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded;
         }
 
         public byte[] SerializeScene(bool kankyo = false)
@@ -222,6 +224,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                     environmentManager.Update();
                     windowManager.Update();
                     effectManager.Update();
+                    sceneManager.Update();
                 }
             }
         }
@@ -321,9 +324,12 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
             lightManager = new LightManager();
             propManager = new PropManager(meidoManager);
             effectManager = new EffectManager();
+            sceneManager = new SceneManager(this);
 
             MaidSwitcherPane maidSwitcherPane = new MaidSwitcherPane(meidoManager);
 
+            SceneWindow sceneWindow = new SceneWindow(sceneManager);
+
             windowManager = new WindowManager()
             {
                 [Constants.Window.Main] = new MainWindow(meidoManager)
@@ -331,10 +337,13 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
                     [Constants.Window.Call] = new CallWindowPane(meidoManager),
                     [Constants.Window.Pose] = new PoseWindowPane(meidoManager, maidSwitcherPane),
                     [Constants.Window.Face] = new FaceWindowPane(meidoManager, maidSwitcherPane),
-                    [Constants.Window.BG] = new BGWindowPane(environmentManager, lightManager, effectManager),
+                    [Constants.Window.BG] = new BGWindowPane(
+                        environmentManager, lightManager, effectManager, sceneWindow
+                    ),
                     [Constants.Window.BG2] = new BG2WindowPane(meidoManager, propManager)
                 },
-                [Constants.Window.Message] = new MessageWindow(messageWindowManager)
+                [Constants.Window.Message] = new MessageWindow(messageWindowManager),
+                [Constants.Window.Save] = sceneWindow
             };
 
             meidoManager.BeginCallMeidos += (s, a) => uiActive = false;
@@ -364,7 +373,7 @@ namespace COM3D2.MeidoPhotoStudio.Plugin
 
         private void Deactivate()
         {
-            if (meidoManager.Busy) return;
+            if (meidoManager.Busy || SceneManager.Busy) return;
 
             ResetCalcNearClip();