using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using ExIni; using UnityEngine; using UnityInjector; using UnityInjector.Attributes; namespace CM3D2.ToukaScreenShot.Plugin { [PluginVersion("0.1.2.0")] [PluginName("Com3d2.ToukaScreenShot.Plugin")] [PluginFilter("COM3D2x64")] public class ToukaScreenShot : PluginBase { private readonly HashSet defaultVisibleLayers = new HashSet { 0, 1, 10 }; private readonly FieldInfo fBloom = typeof(CameraMain).GetField("m_gcBloom", BindingFlags.Instance | BindingFlags.NonPublic); private int layerMask = 0; private bool altKey; private bool bgActiveBack = true; private Color bgColorBack = Color.black; private bool bgVisible = true; private bool bloomBack; private bool bloomOff = true; private bool ctrlKey = true; private string fileNameEnd = ""; private string fileNameHead = "img"; private string folderName = "ScreenShot"; private Camera mainCamera; private int maskBack; private bool noConfigFlg; private bool shiftKey; private string triggerKey = "s"; private string photoShootKey = "d"; private void Start() { for (var i = 0; i < sizeof(int) * 8; i++) if (GetValueIni("Visible", $"Layer{i}", defaultVisibleLayers.Contains(i))) layerMask |= 1 << i; folderName = GetValueIni("File", "Folder", "ScreenShot"); fileNameHead = GetValueIni("File", "Head", "img"); fileNameEnd = GetValueIni("File", "End", ""); bgVisible = GetValueIni("Visible", "BG", false); bloomOff = GetValueIni("Effect", "BloomOff", true); altKey = GetValueIni("Command", "Alt", false); ctrlKey = GetValueIni("Command", "Ctrl", true); shiftKey = GetValueIni("Command", "Shift", false); triggerKey = GetValueIni("Command", "Trigger", "s"); photoShootKey = GetValueIni("Command", "Trigger", "d"); if (noConfigFlg) SaveConfig(); } private T GetValueIni(string section, string key, T @default) { bool TryParse(string value, out T resultVal) { try { resultVal = (T) Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); return true; } catch (Exception e) { Debug.LogWarning($"Failed to parse {section} => {key} value {value} because {e.Message}"); resultVal = default; return false; } } var iniKey = Preferences[section][key]; if (iniKey != null && !string.IsNullOrEmpty(iniKey.Value) && TryParse(iniKey.Value, out var result)) return result; iniKey.Value = Convert.ToString(@default, CultureInfo.InvariantCulture); result = @default; noConfigFlg = true; return result; } private void Update() { var pressedAlt = Input.GetKey(KeyCode.LeftAlt) || Input.GetKey(KeyCode.RightAlt); var pressedCtrl = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl); var pressedShift = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); if (altKey == pressedAlt && ctrlKey == pressedCtrl && shiftKey == pressedShift && Input.GetKeyDown(triggerKey)) StartCoroutine(ExecScreenShot()); else if (altKey == pressedAlt && ctrlKey == pressedCtrl && shiftKey == pressedShift && Input.GetKeyDown(photoShootKey)) StartCoroutine(StartPhotoShoot()); } private IEnumerator StartPhotoShoot() { yield return new WaitForEndOfFrame(); var activeMaidCount = GameMain.Instance.CharacterMgr.GetMaidCount(); Maid activeMaid = null; for (var i = 0; i < activeMaidCount; i++) if ((activeMaid = GameMain.Instance.CharacterMgr.GetMaid(i)).Visible) break; if (activeMaid == null) { Debug.Log("No maid found!"); yield break; } var poseList = Directory.GetFiles(Path.Combine(UTY.gameProjectPath, Path.Combine("PhotoModeData", "MyPose"))); Debug.Log($"Maid to use: {activeMaid.name}"); Debug.Log($"Pose count: {poseList.Length}"); for (var poseNum = 0; poseNum < poseList.Length; poseNum++) { Debug.Log($"{poseNum + 1} / {poseList.Length}"); var poseFile = poseList[poseNum]; PhotoMotionData.AddMyPose(poseFile).Apply(activeMaid); yield return ExecScreenShot(); } Debug.Log("Done!"); } private IEnumerator ExecScreenShot() { yield return new WaitForEndOfFrame(); mainCamera = Camera.main; BackUpCamera(); var ss = GetToukaScreenShot(); if (ss != null) { File.WriteAllBytes(GetTimeFileName(), ss.EncodeToPNG()); Destroy(ss); } // Actually unload textures and GC unused stuff to free up memory yield return Resources.UnloadUnusedAssets(); GC.Collect(); GameMain.Instance.SoundMgr.PlaySe("se022.ogg", false); } private Texture2D GetToukaScreenShot() { var ss = GameMain.Instance.CMSystem.ScreenShotSuperSize switch { CMSystem.SSSuperSizeType.X1 => 1, CMSystem.SSSuperSizeType.X2 => 2, CMSystem.SSSuperSizeType.X4 => 4, _ => 1 }; var aa = GameMain.Instance.CMSystem.Antialias switch { CMSystem.AntiAliasType.None => 0, CMSystem.AntiAliasType.X2 => 2, CMSystem.AntiAliasType.X4 => 4, CMSystem.AntiAliasType.X8 => 8, _ => 8, }; var w = Screen.width * ss; var h = Screen.height * ss; // Cannot use RenderTexture.CreateTemporary as apparently it can fail on lower end PCs var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32); if (aa != 0) rt.antiAliasing = aa; rt.filterMode = FilterMode.Bilinear; SetCameraMask(); mainCamera.backgroundColor = new Color(0f, 0f, 0f, 1f); var blackBgTex = RenderScreenShot(rt, w, h); mainCamera.backgroundColor = new Color(1f, 1f, 1f, 1f); var whiteBgTex = RenderScreenShot(rt, w, h); ResetCamera(); mainCamera.targetTexture = null; rt.Release(); Destroy(rt); var blackPix = blackBgTex.GetPixels32(); var whitePix = whiteBgTex.GetPixels32(); var resultPix = new Color32[blackPix.Length]; for (var i = 0; i < resultPix.Length; i++) { var wp = whitePix[i]; var bp = blackPix[i]; var diff = wp.r - bp.r; if (diff < 0) diff = 0; resultPix[i] = new Color32((byte)((wp.r + bp.r) >> 1), (byte)((wp.g + bp.g) >> 1), (byte)((wp.b + bp.b) >> 1), (byte) ~diff); } var result = new Texture2D(w, h); result.SetPixels32(resultPix); // Mark both for destroying so they can be unloaded by UnloadUnusedAssets Destroy(blackBgTex); Destroy(whiteBgTex); return result; } private string GetTimeFileName() { var targetFolder = Path.Combine(UTY.gameProjectPath, folderName); if (!Directory.Exists(targetFolder)) Directory.CreateDirectory(targetFolder); return Path.Combine(targetFolder, $"{fileNameHead}{DateTime.Now:yyyyMMddHHmmss}{fileNameEnd}.png"); } private void BackUpCamera() { maskBack = mainCamera.cullingMask; bgColorBack = mainCamera.backgroundColor; if (!bgVisible) bgActiveBack = GameMain.Instance.BgMgr.current_bg_object.activeSelf; if (!bloomOff) return; var bloom = (Bloom) fBloom.GetValue(GameMain.Instance.MainCamera); bloomBack = bloom.enabled; } private void ResetCamera() { mainCamera.cullingMask = maskBack; mainCamera.backgroundColor = bgColorBack; if (!bgVisible) GameMain.Instance.BgMgr.current_bg_object.SetActive(bgActiveBack); if (!bloomOff) return; var bloom = (Bloom) fBloom.GetValue(GameMain.Instance.MainCamera); bloom.enabled = bloomBack; } private void SetCameraMask() { mainCamera.cullingMask = layerMask; if (!bgVisible) GameMain.Instance.BgMgr.current_bg_object.SetActive(false); if (!bloomOff) return; var bloom = (Bloom) fBloom.GetValue(GameMain.Instance.MainCamera); bloom.enabled = false; } private Texture2D RenderScreenShot(RenderTexture rt, int w, int h) { mainCamera.targetTexture = rt; mainCamera.Render(); var texture2D = new Texture2D(w, h, TextureFormat.ARGB32, false); var prev = RenderTexture.active; RenderTexture.active = rt; texture2D.ReadPixels(new Rect(0f, 0f, w, h), 0, 0, false); RenderTexture.active = prev; return texture2D; } } }