//#define TEXTURE_FEATURE using System.Text; using System.Numerics; using System.Runtime.InteropServices; using ImGuiNET; using SharpPluginLoader.Core.Memory; using SharpPluginLoader.Core.IO; #if TEXTURE_FEATURE using SharpPluginLoader.Core.Resources; #endif namespace WorldTuningTool { using static Plugin; public static class WorldLUTs { private const string addPath = "nativePC/plugins/CSharp/LUT"; private static string[] builtinList = { "native:\\light\\LUT\\CC_effect_type00_LUTM.tex", "native:\\light\\LUT\\CC_event_A_type00_LUTM.tex", "native:\\light\\LUT\\CC_event_B_type00_LUTM.tex", "native:\\light\\LUT\\CC_event_darkB_type10_LUTM.tex", "native:\\light\\LUT\\CC_event_darkG_type10_LUTM.tex", "native:\\light\\LUT\\CC_event_darkR_type10_LUTM.tex", "native:\\light\\LUT\\CC_event_dark_type00_LUTM.tex", "native:\\light\\LUT\\CC_event_dark_type10_LUTM.tex", "native:\\light\\LUT\\CC_event_evc0001_type00_LUTM.tex", "native:\\light\\LUT\\CC_globalbase_LUTM.tex", "native:\\light\\LUT\\CC_globalbaseSATU_LUTM.tex", "native:\\light\\LUT\\CC_globalbase_stage_LUTM.tex", "native:\\light\\LUT\\CC_st101day_global_type00_LUTM.tex", "native:\\light\\LUT\\CC_st101day_global_type10_LUTM.tex", "native:\\light\\LUT\\CC_st104day_global_type00_LUTM.tex", "native:\\light\\LUT\\CC_st105day_global_type00_LUTM.tex", "native:\\light\\LUT\\CC_title_st101d_type00_LUTM.tex", "native:\\light\\LUT\\CC_title_st102d_type00_LUTM.tex", "native:\\light\\LUT\\CC_title_st103d_type00_LUTM.tex", "native:\\light\\LUT\\CC_title_st104d_type00_LUTM.tex", "native:\\light\\LUT\\CC_title_st105d_type00_LUTM.tex" }; // https://github.com/AsteriskAmpersand/CrappyLUTStudio--CLUTS-/blob/5af10daaa5f295b106cc5db09530a6fe5a4320d4/LUT.py#L35 private static byte[]? parseWorldLUT(BinaryReader reader) { byte[] signature = reader.ReadBytes(4); if (!(signature[0] == 0x54 && signature[1] == 0x45 && signature[2] == 0x58 && signature[3] == 0x0)) { return null; } byte[] skip = reader.ReadBytes(20); uint width = reader.ReadUInt32(); uint height = reader.ReadUInt32(); skip = reader.ReadBytes(8); uint depth = reader.ReadUInt32(); skip = reader.ReadBytes(140); reader.BaseStream.Position = (long)reader.ReadUInt64(); if (!(width == 32 && height == 32 && depth == 32)) { return null; } return reader.ReadBytes((int)(width * height * depth * 8)); } // https://drive.google.com/file/d/143Eh08ZYncCAMwJ1q4gWxVOqR_OSWYvs/view // When does DOMAIN_MIN/MAX apply? private static byte[]? parseCubeLUT(StreamReader reader) { string? line; Vector3[]? parsed = null; int dim = 0, i = 0; while ((line = reader.ReadLine()) != null) { if (line.StartsWith("LUT_3D_SIZE")) { try { dim = Convert.ToInt32(line.Split(' ').Last()); } catch (Exception) { return null; } if (dim != 32) return null; parsed = new Vector3[dim * dim * dim]; while ((line = reader.ReadLine()) != null) { if (line == "") continue; if (line[0] == '0' || line[0] == '1') { break; } } if (line == null) break; } if (parsed != null) { try { parsed[i++] = StringToVector3(line, ' '); } catch (Exception) { return null; } } } if (parsed == null) { return null; } const int lutDataSize = 32 * 32 * 32 * 8; byte[] data = new byte[lutDataSize]; float scale = MathF.Pow(2, 14) - 1; for (i = 0; i < lutDataSize; i += 8) { Vector3 v = parsed[i / 8]; byte[] bytes = BitConverter.GetBytes((ushort)Math.Round(v.X * scale)); data[i] = bytes[0]; data[i + 1] = bytes[1]; bytes = BitConverter.GetBytes((ushort)Math.Round(v.Y * scale)); data[i + 2] = bytes[0]; data[i + 3] = bytes[1]; bytes = BitConverter.GetBytes((ushort)Math.Round(v.Z * scale)); data[i + 4] = bytes[0]; data[i + 5] = bytes[1]; bytes = BitConverter.GetBytes(0x2B88); // Alpha constant. data[i + 6] = bytes[0]; data[i + 7] = bytes[1]; } return data; } private static unsafe bool overwriteFromStream(nint texture, Stream stream, bool cube = false) { byte[]? data = null; if (cube) { using (StreamReader reader = new StreamReader(stream)) { data = parseCubeLUT(reader); } } else { using (BinaryReader reader = new BinaryReader(stream, Encoding.ASCII, false)) { data = parseWorldLUT(reader); } } if (data == null) { return false; } #if TEXTURE_FEATURE fixed (byte *dataPointer = (byte[])data) { // DXGI_FORMAT_R16G16B16A16_FLOAT = 10 return new Texture(texture).Replace(dataPointer, 10, 32, 32, 32); } #else return true; #endif } private static unsafe bool overwriteFromFile(nint texture, string path) { string ext = path.Split('.').Last().ToLower(); using (FileStream stream = File.Open(path, FileMode.Open)) { return overwriteFromStream(texture, stream, ext == "cube"); } } private static unsafe bool overwriteFromMtFile(nint texture, string path) { MtFileStream? file = MtFileStream.FromPath(path, OpenMode.Read, false); bool success = false; if (file != null) { success = overwriteFromStream(texture, new MemoryStream(file.Read(file.Length))); file.Close(); } return success; } private static void reloadBlendLUT(nint lightingObject) { MemoryUtil.GetRef(lightingObject + 0x1D4) = MemoryUtil.GetRef(lightingObject + 0x1D0) + 1.0f; } private static string[] fileList = []; private static string[] selectedFile = [ "", "" ]; private static bool[] selectionStarted = [ false, false ]; private static bool[] selectedInvalid = [ false, false ]; public static void DrawUI(nint texture, nint lightingObject, int i, float width) { ImGui.PushID(i); string lutPath = Marshal.PtrToStringAnsi(texture + 0xC)!; ImGui.Text($"LUT {i}: {lutPath}"); #if TEXTURE_FEATURE if (ImGui.Button("Reset")) { if (overwriteFromMtFile(texture, $"native:\\{lutPath}.tex")) { reloadBlendLUT(lightingObject); } selectedFile[i] = ""; selectedInvalid[i] = false; } ImGui.SameLine(); ImGui.SetNextItemWidth(width * 0.6f); bool prevWasInvalid = selectedInvalid[i]; if (prevWasInvalid) { ImGui.PushStyleColor(ImGuiCol.Text, 0xFF0000FF); } bool beginCombo = ImGui.BeginCombo("##LUT Files", selectedFile[i], ImGuiComboFlags.HeightLarge); if (prevWasInvalid) { ImGui.PopStyleColor(); } if (beginCombo) { foreach (string nativePath in builtinList) { bool isSelected = selectedFile[i] == nativePath; if (ImGui.Selectable(nativePath, isSelected)) { selectedFile[i] = nativePath; if (overwriteFromMtFile(texture, nativePath)) { reloadBlendLUT(lightingObject); } } } if (!selectionStarted[i]) { fileList = Directory.Exists(addPath) ? Directory.GetFiles(addPath) : []; selectionStarted[i] = true; } foreach (string filePath in fileList) { string fileName = Path.GetFileName(filePath); bool isSelected = selectedFile[i] == fileName; if (ImGui.Selectable(fileName, isSelected)) { selectedFile[i] = fileName; if (!(selectedInvalid[i] = !overwriteFromFile(texture, filePath))) { reloadBlendLUT(lightingObject); } } } ImGui.EndCombo(); } else { selectionStarted[i] = false; } #endif ImGui.PopID(); } } }