Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions Classes/AttributeCatalog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Collections.Generic;

namespace Stashie.Classes;

public enum AttrKind { String, Number, Bool, Enum }

public class AttrDef
{
public string Key; // user-facing + ConditionNode.Attribute
public string Dsl; // DSL property, e.g. "ClassName"
public AttrKind Kind;
public string EnumType; // for Enum kind, e.g. "ItemRarity"
public string[] EnumValues; // for Enum kind, e.g. ["Normal","Magic","Rare","Unique"]
public string[] Operators; // operators offered in the UI
}

public static class AttributeCatalog
{
// Operators are also what QueryCompiler understands. Map flags use has/notHas.
public static readonly List<AttrDef> All =
[
new() { Key = "ClassName", Dsl = "ClassName", Kind = AttrKind.String, Operators = ["==","!=","contains"] },
new() { Key = "BaseName", Dsl = "BaseName", Kind = AttrKind.String, Operators = ["==","!=","contains"] },
new() { Key = "Path", Dsl = "Path", Kind = AttrKind.String, Operators = ["contains"] },
new() { Key = "Rarity", Dsl = "Rarity", Kind = AttrKind.Enum, EnumType = "ItemRarity",
EnumValues = ["Normal","Magic","Rare","Unique"], Operators = ["==","!="] },
new() { Key = "ItemLevel", Dsl = "ItemLevel", Kind = AttrKind.Number, Operators = ["==","!=",">=","<=",">","<"] },
new() { Key = "ItemQuality",Dsl = "ItemQuality",Kind = AttrKind.Number, Operators = ["==",">=","<="] },
new() { Key = "Armour", Dsl = "ArmourInfo.Armour", Kind = AttrKind.Number, Operators = ["==",">=","<="] },
new() { Key = "Evasion", Dsl = "ArmourInfo.Evasion", Kind = AttrKind.Number, Operators = ["==",">=","<="] },
new() { Key = "EnergyShield", Dsl = "ArmourInfo.ES", Kind = AttrKind.Number, Operators = ["==",">=","<="] },
new() { Key = "MapTier", Dsl = "MapInfo.Tier", Kind = AttrKind.Number, Operators = ["==",">=","<="] },
new() { Key = "Sockets", Dsl = "SocketInfo.SocketNumber", Kind = AttrKind.Number, Operators = ["==",">="] },
new() { Key = "Links", Dsl = "SocketInfo.LargestLinkSize", Kind = AttrKind.Number, Operators = ["==",">="] },
new() { Key = "StackSize", Dsl = "StackInfo.Count", Kind = AttrKind.Number, Operators = ["==",">=","<="] },

new() { Key = "IsCorrupted", Dsl = "IsCorrupted", Kind = AttrKind.Bool, Operators = ["has","notHas"] },
new() { Key = "IsIdentified", Dsl = "IsIdentified", Kind = AttrKind.Bool, Operators = ["has","notHas"] },
new() { Key = "IsMap", Dsl = "IsMap", Kind = AttrKind.Bool, Operators = ["has","notHas"] },
new() { Key = "IsWeapon", Dsl = "IsWeapon", Kind = AttrKind.Bool, Operators = ["has","notHas"] },
new() { Key = "Enchanted", Dsl = "Enchanted", Kind = AttrKind.Bool, Operators = ["has","notHas"] },
new() { Key = "IsBlightMap", Dsl = "IsBlightMap", Kind = AttrKind.Bool, Operators = ["has","notHas"] },
new() { Key = "IsElderGuardianMap", Dsl = "IsElderGuardianMap", Kind = AttrKind.Bool, Operators = ["has","notHas"] },
];

public static AttrDef Find(string key)
{
foreach (var d in All)
if (d.Key == key) return d;
return null;
}

public static AttrDef FindByDsl(string dsl)
{
foreach (var d in All)
if (d.Dsl == dsl) return d;
return null;
}

// Drops entries whose DSL doesn't compile against the live ItemFilterLibrary build,
// so guessed accessors (MapTier, IsMap, ...) never ship broken. Call once at load.
public static void ProbeAndPrune(System.Action<string> logDropped = null)
{
var kept = new List<AttrDef>();
foreach (var d in All)
{
var probe = d.Kind switch
{
AttrKind.Number => $"{d.Dsl} >= 0",
AttrKind.Bool => d.Dsl,
AttrKind.Enum => $"{d.Dsl} == {d.EnumType}.{d.EnumValues[0]}",
_ => $"{d.Dsl} == \"x\"",
};

if (Compartments.QueryCompiler.Validate(probe).ok)
kept.Add(d);
else
logDropped?.Invoke(d.Key);
}

All.Clear();
All.AddRange(kept);
}
}
32 changes: 32 additions & 0 deletions Classes/ConditionModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections.Generic;

namespace Stashie.Classes;

// Tagged-union node so Newtonsoft (de)serializes without a polymorphic converter.
// Kind == "And"/"Or" -> group, uses Children.
// Kind == "Leaf" -> single condition: a Mod (if Mod != null) or an Attribute.
public class ConditionNode
{
public string Kind = "And"; // "And" | "Or" | "Leaf"
public List<ConditionNode> Children = []; // groups only

// Leaf (attribute) fields:
public string Attribute; // AttributeCatalog key, e.g. "ClassName"
public string Operator; // "==","!=",">=","<=",">","<","contains","has","notHas"
public string Value; // raw operand, compiler types/escapes it
public string Raw; // when non-null on a Leaf: verbatim DSL fragment (round-trip fallback)

public ModRef Mod; // leaf is a mod condition when non-null
}

public class ModRef
{
public string Display; // human chip text, e.g. "+ to Spirit"
public string MatchToken; // FindMods(...) token (group or validated stem)
public string[] GameStats = []; // resolved GameStat names for stat-mode
public bool MatchByStat; // per-condition toggle
public string Mode = "Value";// "Has" | "Value"
public int ValueIndex; // which Values[] index
public int Threshold; // value for "Value" mode
public int? Min, Max; // affix range for the slider
}
119 changes: 119 additions & 0 deletions Classes/ConditionTreeOps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System.Collections.Generic;

namespace Stashie.Classes;

// Pure structural mutations on a ConditionNode tree. No ImGui, no I/O — unit-testable.
// All identity is reference identity (ConditionNode does not override Equals).
public static class ConditionTreeOps
{
public static bool IsGroup(ConditionNode n) => n != null && (n.Kind == "And" || n.Kind == "Or");

public static Dictionary<ConditionNode, ConditionNode> BuildParentMap(ConditionNode root)
{
var map = new Dictionary<ConditionNode, ConditionNode>();
void Walk(ConditionNode n)
{
if (n?.Children == null) return;
foreach (var c in n.Children) { map[c] = n; Walk(c); }
}
Walk(root);
return map;
}

// All groups in pre-order (root first).
public static List<ConditionNode> Groups(ConditionNode root)
{
var list = new List<ConditionNode>();
void Walk(ConditionNode n)
{
if (!IsGroup(n)) return;
list.Add(n);
if (n.Children == null) return;
foreach (var c in n.Children) Walk(c);
}
Walk(root);
return list;
}

// True if 'ancestor' is 'node' or contains it anywhere in its subtree.
public static bool Contains(ConditionNode ancestor, ConditionNode node)
{
if (ancestor == null) return false;
if (ReferenceEquals(ancestor, node)) return true;
if (ancestor.Children == null) return false;
foreach (var c in ancestor.Children)
if (Contains(c, node)) return true;
return false;
}

// Replace 'node' in parent.Children with a new group of 'kind' that contains 'node'.
public static ConditionNode Wrap(ConditionNode parent, ConditionNode node, string kind)
{
if (parent?.Children == null) return null;
var idx = parent.Children.IndexOf(node);
if (idx < 0) return null;
var group = new ConditionNode { Kind = kind, Children = [node] };
parent.Children[idx] = group;
return group;
}

public static void Convert(ConditionNode group)
{
if (!IsGroup(group)) return;
if (group.Kind == "And") group.Kind = "Or";
else group.Kind = "And";
}

// Reparent 'node' to the end of 'dest'. Rejected if dest is not a group, or moving
// would create a cycle (dest == node or a descendant of node), or dest is the current parent.
public static bool Move(ConditionNode root, ConditionNode node, ConditionNode dest)
{
if (!IsGroup(dest)) return false;
if (Contains(node, dest)) return false;
var map = BuildParentMap(root);
if (!map.TryGetValue(node, out var parent)) return false;
if (ReferenceEquals(parent, dest)) return false;
parent.Children.Remove(node);
dest.Children.Add(node);
return true;
}

// Safe only when parent has the same connective, or the group has <=1 child —
// otherwise inlining would silently change boolean meaning.
public static bool CanUngroup(ConditionNode root, ConditionNode group)
{
if (!IsGroup(group)) return false;
var map = BuildParentMap(root);
if (!map.TryGetValue(group, out var parent)) return false; // root can't ungroup
return parent.Kind == group.Kind || group.Children.Count <= 1;
}

public static bool Ungroup(ConditionNode root, ConditionNode group)
{
if (!CanUngroup(root, group)) return false;
var parent = BuildParentMap(root)[group];
var idx = parent.Children.IndexOf(group);
parent.Children.RemoveAt(idx);
parent.Children.InsertRange(idx, group.Children);
return true;
}

public static bool Delete(ConditionNode root, ConditionNode node)
{
var map = BuildParentMap(root);
if (!map.TryGetValue(node, out var parent)) return false;
return parent.Children.Remove(node);
}

// Replace 'oldNode' with 'newNode' in its parent. Returns true if replaced.
public static bool Replace(ConditionNode root, ConditionNode oldNode, ConditionNode newNode)
{
if (newNode == null) return false;
var map = BuildParentMap(root);
if (!map.TryGetValue(oldNode, out var parent)) return false;
var idx = parent.Children.IndexOf(oldNode);
if (idx < 0) return false;
parent.Children[idx] = newNode;
return true;
}
}
13 changes: 13 additions & 0 deletions Classes/FilterEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ public class Filter
public string FilterName;
public string RawQuery;
public bool Shifting = false;

public string Id; // NEW: stable GUID; assigned on create/load
public ConditionNode Conditions; // NEW: optional structured tree (null => raw-only)

// Runtime-only compile state for the builder; ignored by both serializers
// (Newtonsoft for filter files, System.Text.Json for settings persistence).
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public string _lastCompiled;

[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
public bool? _lastCompileOk;
}
}

Expand Down
58 changes: 58 additions & 0 deletions Classes/ItemFingerprint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ExileCore2.PoEMemory.Components;
using ExileCore2.PoEMemory.MemoryObjects;

namespace Stashie.Classes;

/// <summary>
/// Builds a stable identity string ("fingerprint") for an inventory item, used by the
/// dynamic-ignore feature to recognise the same item regardless of which cell it occupies.
/// Pure and stateless: same item state always yields the same string.
/// </summary>
public static class ItemFingerprint
{
/// <summary>
/// Returns a stable fingerprint for the given item entity, or null if the entity is null.
/// Falls back to the entity path when the item has no Mods component.
/// The fingerprint combines base path + rarity + unique name + every implicit/explicit/
/// corruption/enchant mod's RawName and roll Values (exact-item matching).
/// </summary>
public static string Build(Entity itemEntity)
{
if (itemEntity == null)
return null;

var mods = itemEntity.GetComponent<Mods>();
if (mods == null)
return itemEntity.Path;

var tokens = new List<string>
{
$"B:{itemEntity.Path}",
$"R:{mods.ItemRarity}",
$"U:{mods.UniqueName}"
};

AppendMods(tokens, "I", mods.ImplicitMods);
AppendMods(tokens, "E", mods.ExplicitMods);
AppendMods(tokens, "C", mods.CorruptionImplicitMods);
AppendMods(tokens, "N", mods.EnchantedMods);

return string.Join("|", tokens);
}

private static void AppendMods(List<string> tokens, string prefix, List<ItemMod> mods)
{
if (mods == null || mods.Count == 0)
return;

// Sort within each category so the framework's memory ordering can't change the result.
var modTokens = mods
.Select(m => $"{prefix}:{m.RawName}:{string.Join(",", m.Values ?? new List<int>())}")
.OrderBy(s => s, StringComparer.Ordinal);

tokens.AddRange(modTokens);
}
}
Loading