using System.Numerics; using ImGuiNET; using SharpPluginLoader.Core.Memory; namespace WorldTuningTool { using static Utility; using static PFuncs; public enum ParameterType { BOOL, FLAG, BYTE, INT, ALIGNED_INT, HEX, BITS, COLOR, COLOR_FACTOR, FLOAT, VECTOR3, VECTOR2, VECTOR4, PATCH_FLOAT, SHADOW_RESOLUTION } public static class ParameterFlags { public const int ViewOnly = 1; } public static class PFuncs { public static bool IgnoreMinMax = false; public static bool InTransition = false; public static List DirtyParams = new(); public static void BeginTransition() { Assert(!InTransition && DirtyParams.Count == 0); InTransition = true; } public static void EndTransition() { Assert(InTransition); InTransition = false; foreach (Parameter param in DirtyParams) { param.Resolve(); } DirtyParams.Clear(); } public static Action? HandleShadowResolution = null; public static Action? DrawStageOverrideTooltip = null; public static Action? DrawExtraButtons = null; public static Parameter P(string name, nint offset, ParameterType type, (T? min, T? max, T? step) m = default) where T : unmanaged { return new Parameter(name, offset, 0, type, m.min, m.max, m.step, 0); } public static Parameter P(string name, long offset, ParameterType type, (T? min, T? max, T? step) m = default) where T : unmanaged { return new Parameter(name, (nint)offset, 0, type, m.min, m.max, m.step, 0); } public static Parameter V(string name, nint offset, ParameterType type, (T? min, T? max, T? step) m = default) where T : unmanaged { return new Parameter(name, offset, 0, type, m.min, m.max, m.step, ParameterFlags.ViewOnly); } public static Parameter M(string name, nint offset, ParameterType type, int mask, (T? min, T? max, T? step) m = default) where T : unmanaged { return new Parameter(name, offset, mask, type, m.min, m.max, m.step, 0); } public static (float?, float?, float?) pStep(float step) { return (null, null, step); } public static (float?, float?, float?) pMin(float min) { return (min, null, null); } public static (float?, float?, float?) pMax(float max) { return (null, max, null); } public static (float?, float?, float?) pMinStep(float min, float step) { return (min, null, step); } public static (float?, float?, float?) pMaxStep(float max, float step) { return (null, max, step); } public static (float?, float?, float?) pMinMaxStep(float min, float max, float step) { return (min, max, step); } public static (int?, int?, int?) pStep(int step) { return (null, null, step); } public static (int?, int?, int?) pMin(int min) { return (min, null, null); } public static (int?, int?, int?) pMax(int max) { return (null, max, null); } public static (int?, int?, int?) pMinStep(int min, int step) { return (min, null, step); } public static (int?, int?, int?) pMaxStep(int max, int step) { return (null, max, step); } public static (int?, int?, int?) pMinMaxStep(int min, int max, int step) { return (min, max, step); } public static (byte?, byte?, byte?) pStep(byte step) { return (null, null, step); } } public abstract class Parameter { public string Name; private string hiddenName; public ParameterType Type; private nint offset; private int mask; private nint lastAddr = 0x0; private bool overrideValue = false; private bool overrideForStage = false; private bool overrideWasOn = false; private int flags; public bool ViewOnly => (flags & ParameterFlags.ViewOnly) == ParameterFlags.ViewOnly; protected float stepf, minf, maxf; protected int step, min, max; private bool fallbackUpdate = true; private bool pendingWrite = false; private Vector4 valueV = default; private int valueInt = 0; private Vector4 oValueV; // Original value. private int oValueInt; private bool pendingUpdate = true; private bool queuedOverride = false; private bool queuedValue = false; private Vector4 qValueV; // Queued value. private int qValueInt; private bool bValue = false; private Vector4 bValueV; // B value in A/B. private int bValueInt; public Parameter(string name, nint offset, int mask, ParameterType type, int flags) { Name = name; hiddenName = "##" + Name; Type = type; switch (Type) { case ParameterType.FLAG: Assert(mask != 0); break; case ParameterType.PATCH_FLOAT: valueV.X = Get(offset); pendingUpdate = false; break; case ParameterType.SHADOW_RESOLUTION: valueInt = 1; pendingUpdate = false; break; } this.offset = offset; this.mask = mask; this.flags = flags; } public bool PendingUpdate() { return pendingUpdate; } public (Vector4, int) GetValue() { return (valueV, valueInt); } public int GetMask() { return mask; } public void Resolve() { Assert(!InTransition && pendingWrite); pendingWrite = false; maybeWriteCurrentValue(); } private void maybeWriteCurrentValue() { if (InTransition) { if (!DirtyParams.Contains(this)) { pendingWrite = true; DirtyParams.Add(this); } else { Assert(pendingWrite); } return; } Assert(!pendingWrite); switch (Type) { case ParameterType.PATCH_FLOAT: new Patch(offset, BitConverter.GetBytes(valueV.X)).Enable(); break; case ParameterType.SHADOW_RESOLUTION: if (HandleShadowResolution != null) { HandleShadowResolution(valueInt); } break; } if (lastAddr == 0x0) { return; } switch (Type) { case ParameterType.BOOL: case ParameterType.BYTE: GetRef(lastAddr, offset) = (byte)valueInt; break; case ParameterType.FLAG: { ref int i1 = ref GetRef(lastAddr, offset); if (valueInt != 0) { i1 |= mask; } else { i1 &= ~mask; } break; } case ParameterType.INT: case ParameterType.ALIGNED_INT: case ParameterType.HEX: case ParameterType.BITS: case ParameterType.COLOR: { ref int i1 = ref GetRef(lastAddr, offset); if (mask != 0) { i1 = (i1 & ~mask) | ((valueInt << BitIndex(mask)) & mask); } else { i1 = valueInt; } break; } case ParameterType.FLOAT: GetRef(lastAddr, offset) = valueV.X; break; case ParameterType.VECTOR2: GetRef(lastAddr, offset) = new(valueV.X, valueV.Y); break; case ParameterType.VECTOR3: case ParameterType.COLOR_FACTOR: GetRef(lastAddr, offset) = new(valueV.X, valueV.Y, valueV.Z); break; case ParameterType.VECTOR4: GetRef(lastAddr, offset) = valueV; break; } } public void ForceOverrideOn() { if (!queuedOverride && !overrideValue) { OverrideOn(false); } } public void ForceOverrideOff() { if (queuedOverride || overrideValue) { OverrideOff(); } } public void OverrideOn(bool forStage) { if (pendingUpdate) { if (queuedOverride) { Assert(!overrideWasOn && forStage); overrideWasOn = true; } else { queuedOverride = true; } } else { if (overrideValue) { Assert(!overrideWasOn && forStage); overrideWasOn = true; } else { oValueV = valueV; oValueInt = valueInt; maybeWriteCurrentValue(); overrideValue = true; } } overrideForStage = forStage; } public void OverrideOff() { if (queuedOverride) { if (overrideWasOn) { Assert(overrideForStage); overrideWasOn = false; } else { queuedOverride = false; } } else { Assert(overrideValue); if (overrideWasOn) { Assert(overrideForStage); overrideWasOn = false; } else { valueV = oValueV; valueInt = oValueInt; maybeWriteCurrentValue(); bValue = false; overrideValue = false; } } overrideForStage = false; } public void SetOverrideValue(Vector4 v4, int i1) { if (queuedOverride) { qValueV = v4; qValueInt = i1; queuedValue = true; } else { valueV = v4; valueInt = i1; maybeWriteCurrentValue(); } } public void Update(nint baseObject, bool fromHook = false) { lastAddr = baseObject; if (baseObject == 0x0) { pendingUpdate = true; return; } if (fromHook) { fallbackUpdate = false; } else if (!fallbackUpdate) { fallbackUpdate = true; return; } // Trying to track changes to the original value is a "best effort" // approach because it can't work if the in-game value is updated // to the same value as a current override. switch (Type) { case ParameterType.BOOL: case ParameterType.BYTE: { ref byte b1 = ref GetRef(baseObject, offset); byte b = b1; if (overrideValue) { if (b != (byte)valueInt) { oValueInt = b; } b1 = (byte)valueInt; } else { valueInt = b; } break; } case ParameterType.FLAG: { ref int i1 = ref GetRef(baseObject, offset); int b1 = ByteFlag((i1 & mask) == mask); if (overrideValue) { if (b1 != valueInt) { oValueInt = b1; } if (valueInt != 0) { i1 |= mask; } else { i1 &= ~mask; } } else { valueInt = b1; } break; } case ParameterType.INT: case ParameterType.ALIGNED_INT: case ParameterType.HEX: case ParameterType.BITS: case ParameterType.COLOR: { ref int i1 = ref GetRef(baseObject, offset); int m = i1; if (mask != 0) { m = (m & mask) >> BitIndex(mask); } if (overrideValue) { if (m != valueInt) { oValueInt = m; } if (mask != 0) { i1 = (i1 & ~mask) | ((valueInt << BitIndex(mask)) & mask); } else { i1 = valueInt; } } else { valueInt = m; } break; } case ParameterType.FLOAT: { ref float f1 = ref GetRef(baseObject, offset); float f = f1; if (overrideValue) { if (f != valueV.X) { oValueV.X = f; } f1 = valueV.X; } else { valueV.X = f; } break; } case ParameterType.VECTOR2: { ref Vector2 v2 = ref GetRef(baseObject, offset); Vector2 v = v2; if (overrideValue) { Vector2 valueV2 = new(valueV.X, valueV.Y); if (v != valueV2) { oValueV = new Vector4(v.X, v.Y, 0.0f, 0.0f); } v2 = valueV2; } else { valueV = new Vector4(v.X, v.Y, 0.0f, 0.0f); } break; } case ParameterType.VECTOR3: case ParameterType.COLOR_FACTOR: { ref Vector3 v3 = ref GetRef(baseObject, offset); Vector3 v = v3; if (overrideValue) { Vector3 valueV3 = new(valueV.X, valueV.Y, valueV.Z); if (v != valueV3) { oValueV = new Vector4(v.X, v.Y, v.Z, 0.0f); } v3 = valueV3; } else { valueV = new Vector4(v.X, v.Y, v.Z, 0.0f); } break; } case ParameterType.VECTOR4: { ref Vector4 v4 = ref GetRef(baseObject, offset); Vector4 v = v4; if (overrideValue) { if (v != valueV) { oValueV = v; } v4 = valueV; } else { valueV = v; } break; } } if (pendingUpdate) { pendingUpdate = false; if (queuedOverride) { Assert(!overrideValue); OverrideOn(overrideForStage); if (queuedValue) { valueV = qValueV; valueInt = qValueInt; maybeWriteCurrentValue(); queuedValue = false; } queuedOverride = false; } } } public void AdjustValue(ref Vector4 v4, ref int i1) { if (Type == ParameterType.SHADOW_RESOLUTION) // Hard limit. { #if OVERSIZED_SHADOW_MAP i1 = Math.Clamp(i1, 1, 6); #else i1 = Math.Clamp(i1, 1, 5); #endif } else if (!IgnoreMinMax) { switch (Type) { case ParameterType.BOOL: case ParameterType.FLAG: case ParameterType.BYTE: case ParameterType.INT: case ParameterType.HEX: case ParameterType.BITS: i1 = Math.Clamp(i1, min, max); break; case ParameterType.ALIGNED_INT: i1 = Math.Clamp((i1 + (step - 1))&~(step - 1), min, max); break; case ParameterType.FLOAT: case ParameterType.PATCH_FLOAT: case ParameterType.VECTOR2: case ParameterType.VECTOR3: case ParameterType.COLOR_FACTOR: case ParameterType.VECTOR4: v4 = new Vector4( Math.Clamp(v4.X, minf, maxf), Math.Clamp(v4.Y, minf, maxf), Math.Clamp(v4.Z, minf, maxf), Math.Clamp(v4.W, minf, maxf)); break; } } } public bool DrawValue(ref Vector4 v4, ref int i1, float width, bool drawLabel) { bool valueChanged = false; string label = drawLabel ? Name : hiddenName; float displayMinf = IgnoreMinMax ? 0.0f : minf; float displayMaxf = IgnoreMinMax ? 0.0f : maxf; switch (Type) { case ParameterType.BOOL: case ParameterType.FLAG: { bool b = i1 != 0; if (ImGui.Checkbox(label, ref b)) { i1 = ByteFlag(b); valueChanged = true; } break; } case ParameterType.BYTE: { ImGuiInputTextFlags flags = ImGuiInputTextFlags.EnterReturnsTrue; ImGui.SetNextItemWidth(width * defaultParamWidth); if (ImGui.InputInt(label, ref i1, step, 0, flags)) { valueChanged = true; } break; } case ParameterType.INT: case ParameterType.ALIGNED_INT: case ParameterType.HEX: case ParameterType.SHADOW_RESOLUTION: { ImGuiInputTextFlags flags = ImGuiInputTextFlags.EnterReturnsTrue; if (Type == ParameterType.HEX) { flags |= ImGuiInputTextFlags.CharsHexadecimal; } ImGui.SetNextItemWidth(width * defaultParamWidth); if (ImGui.InputInt(label, ref i1, step, 0, flags)) { valueChanged = true; } break; } case ParameterType.BITS: { for (int i = 0; i < 32; i++) { int bit = (1 << i); if ((mask & bit) == bit) { bool set = (i1 & bit) == bit; if (ImGui.Checkbox($"##Bit{i}", ref set)) { if (set) { i1 |= bit; } else { i1 &= ~bit; } valueChanged = true; } ImGui.SameLine(); } } ImGui.Text(label); break; } case ParameterType.COLOR: { Vector4 colorV = ColorVectorFromInt(i1); ImGui.SetNextItemWidth(width * defaultParamWidth * 3.0f); if (ImGui.ColorEdit4(label, ref colorV)) { i1 = ColorVectorToInt(colorV); valueChanged = true; } break; } case ParameterType.FLOAT: case ParameterType.PATCH_FLOAT: { float f1 = v4.X; ImGui.SetNextItemWidth(width * defaultParamWidth); if (ImGui.DragFloat(label, ref f1, stepf, displayMinf, displayMaxf, "%.6f", ImGuiSliderFlags.NoRoundToFormat)) { v4 = new Vector4(f1, 0.0f, 0.0f, 0.0f); valueChanged = true; } break; } case ParameterType.VECTOR2: { Vector2 v2 = new(v4.X, v4.Y); ImGui.SetNextItemWidth(width * defaultParamWidth * 2.0f); if (ImGui.DragFloat2(label, ref v2, stepf, displayMinf, displayMaxf, "%.5f", ImGuiSliderFlags.NoRoundToFormat)) { v4 = new Vector4(v2.X, v2.Y, 0.0f, 0.0f); valueChanged = true; } break; } case ParameterType.VECTOR3: case ParameterType.COLOR_FACTOR: { Vector3 v3 = new(v4.X, v4.Y, v4.Z); if (Type == ParameterType.COLOR_FACTOR) { Vector3 v3Color = v3; // Only scale the color vector if max is unbound. Otherwise, assume that intensity and color are seperate. if (maxf == Single.MaxValue) { float v3Max = MathF.Max(Math.Abs(v3.X), MathF.Max(Math.Abs(v3.Y), Math.Abs(v3.Z))); if (v3Max != 0.0f) { // I believe a compromise is needed here. These are the options I can think of. //v3Color /= v3Max; // Preview will be full "brightness" at low intensities. v3Color = v3Color * (v3Color / v3Max); // Preview will converge towards white at high intensities. } } ImGui.ColorEdit3(label, ref v3Color, ImGuiColorEditFlags.NoPicker | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.NoInputs); ImGui.SameLine(); } ImGui.SetNextItemWidth(width * defaultParamWidth * 3.0f); if (ImGui.DragFloat3(label, ref v3, stepf, displayMinf, displayMaxf, "%.5f", ImGuiSliderFlags.NoRoundToFormat)) { v4 = new Vector4(v3.X, v3.Y, v3.Z, 0.0f); valueChanged = true; } break; } case ParameterType.VECTOR4: { ImGui.SetNextItemWidth(width * defaultParamWidth * 4.0f); if (ImGui.DragFloat4(label, ref v4, stepf, displayMinf, displayMaxf, "%.5f", ImGuiSliderFlags.NoRoundToFormat)) { valueChanged = true; } break; } } if (valueChanged) { AdjustValue(ref v4, ref i1); } return valueChanged; } public void DrawReferenceValue(string name, Vector4 v4, int i1) { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * fadeAlphaFactor); switch (Type) { case ParameterType.BOOL: case ParameterType.FLAG: ImGui.Text($"({name}: {i1 != 0})"); break; case ParameterType.BYTE: case ParameterType.INT: case ParameterType.ALIGNED_INT: case ParameterType.SHADOW_RESOLUTION: ImGui.Text($"({name}: {i1})"); break; case ParameterType.HEX: case ParameterType.BITS: case ParameterType.COLOR: ImGui.Text($"({name}: {i1:X})"); break; case ParameterType.FLOAT: case ParameterType.PATCH_FLOAT: ImGui.Text($"({name}: {v4.X:0.000000})"); break; case ParameterType.VECTOR2: ImGui.Text($"({name}: {v4.X:0.00000}, {v4.Y:0.00000})"); break; case ParameterType.VECTOR3: case ParameterType.COLOR_FACTOR: ImGui.Text($"({name}: {v4.X:0.00000}, {v4.Y:0.00000}, {v4.Z:0.00000})"); break; case ParameterType.VECTOR4: ImGui.Text($"({name}: {v4.X:0.00000}, {v4.Y:0.00000}, {v4.Z:0.00000}, {v4.W:0.00000})"); break; } ImGui.PopStyleVar(); } public void Draw(float width, bool assumeOverride = false) { Draw(ref valueV, ref valueInt, width, assumeOverride); } public void Draw(ref Vector4 v4, ref int i1, float width, bool assumeOverride) { ImGui.PushID(Name); if (ViewOnly) { ImGui.SetCursorPos(ImGui.GetCursorPos() + new Vector2(ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.X, 0.0f)); } else if (!assumeOverride) { if (overrideForStage) { ImGui.PushItemFlag(ImGuiItemFlags.Disabled, true); ImGui.PushItemFlag(ImGuiItemFlags.MixedValue, true); } // If OverrideOn() were to set queuedOverride instead of overrideValue, this parameter shouldn't be drawn. Assert(!PendingUpdate()); bool toggleOverride = overrideValue; if (ImGui.Checkbox("##Override", ref toggleOverride)) { if (toggleOverride) { OverrideOn(false); } else { OverrideOff(); } } if (ImGui.BeginItemTooltip()) { if (overrideForStage && DrawStageOverrideTooltip != null) { DrawStageOverrideTooltip(this); } else if (overrideValue) { ImGui.Text("Set"); } else { ImGui.Text("Set override"); } ImGui.EndTooltip(); } if (overrideForStage) { ImGui.PopItemFlag(); ImGui.PopItemFlag(); } ImGui.SameLine(); } if (!overrideValue) { ImGui.PushItemFlag(ImGuiItemFlags.Disabled, true); } if (DrawValue(ref valueV, ref valueInt, width, !bValue)) { maybeWriteCurrentValue(); } if (!overrideValue) { ImGui.PopItemFlag(); } bool showButtons = !assumeOverride && overrideValue && !pendingUpdate; if (showButtons) { ImGui.SameLine(); if (!bValue) { if (ImGui.Button("B")) { bValueV = oValueV; bValueInt = oValueInt; bValue = true; } } else { if (ImGui.Button("A/B")) { (bValueV, valueV) = (valueV, bValueV); (bValueInt, valueInt) = (valueInt, bValueInt); maybeWriteCurrentValue(); } ImGui.SameLine(); DrawValue(ref bValueV, ref bValueInt, width, true); ImGui.SameLine(); if (ImGui.Button("< B")) { bValue = false; } } if (DrawExtraButtons != null) { DrawExtraButtons(this); } ImGui.SameLine(); DrawReferenceValue("Original", oValueV, oValueInt); } ImGui.PopID(); } } public class Parameter : Parameter where T : unmanaged { public Parameter(string name, nint offset, int mask, ParameterType type, T? min, T? max, T? step, int flags) : base(name, offset, mask, type, flags) { if (typeof(T) == typeof(float)) { Assert(type == ParameterType.FLOAT || type == ParameterType.VECTOR2 || type == ParameterType.VECTOR3 || type == ParameterType.VECTOR4 || type == ParameterType.PATCH_FLOAT || type == ParameterType.COLOR_FACTOR); this.stepf = (step != null) ? Convert.ToSingle(step) : 0.1f; this.minf = (min != null) ? Convert.ToSingle(min) : -Single.MaxValue; this.maxf = (max != null) ? Convert.ToSingle(max) : Single.MaxValue; } else { this.step = (step != null) ? Convert.ToInt32(step) : 0; if (typeof(T) == typeof(int)) { Assert(type == ParameterType.INT || type == ParameterType.ALIGNED_INT || type == ParameterType.HEX || type == ParameterType.BITS || type == ParameterType.COLOR || type == ParameterType.SHADOW_RESOLUTION); this.min = (min != null) ? Convert.ToInt32(min) : Int32.MinValue; this.max = (max != null) ? Convert.ToInt32(max) : Int32.MaxValue; } else if (typeof(T) == typeof(byte)) { Assert(type == ParameterType.BYTE); this.min = (min != null) ? Convert.ToInt32(min) : 0x0; this.max = (max != null) ? Convert.ToInt32(max) : 0xFF; } else if (typeof(T) == typeof(bool)) { Assert(type == ParameterType.BOOL || type == ParameterType.FLAG); this.min = 0; this.max = 1; } else { Assert(); } } } } }