//#define TEXTURE_FEATURE using System.Text; using System.Numerics; using System.Runtime.InteropServices; using ImGuiNET; using SharpPluginLoader.Core.Memory; using SharpPluginLoader.Core.Resources; using SharpPluginLoader.Core.IO; namespace WorldTuningTool { public class WorldLUTs { private const string addPath = "nativePC/plugins/CSharp/LUT"; public WorldLUTs() { } // https://github.com/AsteriskAmpersand/CrappyLUTStudio--CLUTS-/blob/5af10daaa5f295b106cc5db09530a6fe5a4320d4/LUT.py#L35 private byte[]? parseWorldLUT(BinaryReader reader) { byte[] signature = reader.ReadBytes(4); if (!(signature[0] == 0x54 && signature[1] == 0x45 && (signature[2] == 0x58 && signature[3] == 0x0))) { return null; } ulong version = reader.ReadUInt64(); uint datablock = reader.ReadUInt32(); uint format = reader.ReadUInt32(); uint mipCount = reader.ReadUInt32(); uint width = reader.ReadUInt32(); uint height = reader.ReadUInt32(); uint mipListCount = reader.ReadUInt32(); uint typeData = reader.ReadUInt32(); uint depth = reader.ReadUInt32(); byte[] NULL0 = reader.ReadBytes(12); int NEG0 = reader.ReadInt32(); byte[] NULL1 = reader.ReadBytes(8); int Special = reader.ReadInt32(); byte[] NULL2 = reader.ReadBytes(16); byte[] NEG1 = reader.ReadBytes(32); byte[] flags = reader.ReadBytes(32); byte[] NULLX = reader.ReadBytes(32); 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 byte[]? parseCubeLUT(StreamReader reader) { string? line; Vector3[]? parsed = null; int dim = 0, idx = 0; while ((line = reader.ReadLine()) != null) { if (line.StartsWith("LUT_3D_SIZE")) { try { dim = Convert.ToInt32(line.Split(' ').Last()); } catch (FormatException) { return null; } catch (OverflowException) { 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[idx] = Plugin.StringToVector3(line, ' '); } catch (FormatException) { return null; } catch (OverflowException) { return null; } idx++; } } 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 (idx = 0; idx < lutDataSize; idx+=8) { Vector3 v = parsed[idx/8]; ushort normX = (ushort)Math.Round(v.X * scale); ushort normY = (ushort)Math.Round(v.Y * scale); ushort normZ = (ushort)Math.Round(v.Z * scale); byte[] bytes = BitConverter.GetBytes(normX); data[idx] = bytes[0]; data[idx + 1] = bytes[1]; bytes = BitConverter.GetBytes(normY); data[idx + 2] = bytes[0]; data[idx + 3] = bytes[1]; bytes = BitConverter.GetBytes(normZ); data[idx + 4] = bytes[0]; data[idx + 5] = bytes[1]; bytes = BitConverter.GetBytes(0x2B88); // Alpha constant. data[idx + 6] = bytes[0]; data[idx + 7] = bytes[1]; } return data; } private 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 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 void reloadBlendLUT(nint lightingObject) { MemoryUtil.GetRef(lightingObject + 0x1D4) = MemoryUtil.GetRef(lightingObject + 0x1D0) + 1.0f; } private string[] fileList = []; private static string[] selectedFile = [ "", "" ]; private static bool[] selectionStarted = [ false, false ]; private static bool[] selectedInvalid = [ false, false ]; public void DrawLUT(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")) { MtFileStream? file = MtFileStream.FromPath($"native:\\{lutPath}.tex", OpenMode.Read, false); if (file != null) { if (overwriteFromStream(texture, new MemoryStream(file.Read(file.Length)))) { reloadBlendLUT(lightingObject); } file.Close(); } selectedFile[i] = ""; selectedInvalid[i] = false; } ImGui.SameLine(); ImGui.SetNextItemWidth(width * 0.6f); bool prevWasInvalid = selectedInvalid[i]; if (prevWasInvalid) { ImGui.PushStyleColor(ImGuiCol.Text, 0xFF0000FF); } if (ImGui.BeginCombo("##LUT Files", selectedFile[i], ImGuiComboFlags.HeightLarge)) { if (prevWasInvalid) { ImGui.PopStyleColor(); } 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 { if (prevWasInvalid) { ImGui.PopStyleColor(); } selectionStarted[i] = false; } #endif ImGui.PopID(); } } }