From 5658a33e763cb3ed296f683b5dd128dc94f113e9 Mon Sep 17 00:00:00 2001 From: Andrew Opalach Date: Wed, 21 Oct 2026 17:18:23 -0400 Subject: Factor out Parameter class Make it less dependent on Plugin specific code so it can be more easily reused. Signed-off-by: Andrew Opalach --- Parameter.cs | 950 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 950 insertions(+) create mode 100644 Parameter.cs (limited to 'Parameter.cs') diff --git a/Parameter.cs b/Parameter.cs new file mode 100644 index 0000000..db564a3 --- /dev/null +++ b/Parameter.cs @@ -0,0 +1,950 @@ +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(); + } + } + } + } +} -- cgit v1.2.3-101-g0448