Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

made config serializing more efficient (hopefully) #537

Merged
merged 2 commits into from
Jan 2, 2025
Merged
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
14 changes: 7 additions & 7 deletions BossMod/BossModule/AIHints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public enum SpecialMode
public Bitmap.Region PathfindMapObstacles;

// list of potential targets
public List<Enemy> PotentialTargets = [];
public readonly List<Enemy> PotentialTargets = [];
private int potentialTargetsCount;
public int HighestPotentialTargetPriority;

Expand All @@ -56,13 +56,13 @@ public enum SpecialMode

// positioning: list of shapes that are either forbidden to stand in now or will be in near future
// AI will try to move in such a way to avoid standing in any forbidden zone after its activation or outside of some restricted zone after its activation, even at the cost of uptime
public List<(Func<WPos, float> shapeDistance, DateTime activation)> ForbiddenZones = [];
public readonly List<(Func<WPos, float> shapeDistance, DateTime activation)> ForbiddenZones = [];

// positioning: list of goal functions
// AI will try to move to reach non-forbidden point with highest goal value (sum of values returned by all functions)
// guideline: rotation modules should return 1 if it would use single-target action from that spot, 2 if it is also a positional, 3 if it would use aoe that would hit minimal viable number of targets, +1 for each extra target
// other parts of the code can return small (e.g. 0.01) values to slightly (de)prioritize some positions, or large (e.g. 1000) values to effectively soft-override target position (but still utilize pathfinding)
public List<Func<WPos, float>> GoalZones = [];
public readonly List<Func<WPos, float>> GoalZones = [];

// positioning: next positional hint (TODO: reconsider, maybe it should be a list prioritized by in-gcds, and imminent should be in-gcds instead? or maybe it should be property of an enemy? do we need correct?)
public (Actor? Target, Positional Pos, bool Imminent, bool Correct) RecommendedPositional;
Expand All @@ -73,7 +73,7 @@ public void SetPositional(Positional positional)

// orientation restrictions (e.g. for gaze attacks): a list of forbidden orientation ranges, now or in near future
// AI will rotate to face allowed orientation at last possible moment, potentially losing uptime
public List<(Angle center, Angle halfWidth, DateTime activation)> ForbiddenDirections = [];
public readonly List<(Angle center, Angle halfWidth, DateTime activation)> ForbiddenDirections = [];

// closest special movement/targeting/action mode, if any
public (SpecialMode mode, DateTime activation) ImminentSpecialMode;
Expand All @@ -83,17 +83,17 @@ public void SetPositional(Positional positional)

// predicted incoming damage (raidwides, tankbusters, etc.)
// AI will attempt to shield & mitigate
public List<(BitMask players, DateTime activation)> PredictedDamage = [];
public readonly List<(BitMask players, DateTime activation)> PredictedDamage = [];

// estimate of the maximal time we can spend casting before we need to move
// TODO: reconsider...
public float MaxCastTimeEstimate = float.MaxValue;

// actions that we want to be executed, gathered from various sources (manual input, autorotation, planner, ai, modules, etc.)
public ActionQueue ActionsToExecute = new();
public readonly ActionQueue ActionsToExecute = new();

// buffs to be canceled asap
public List<(uint statusId, ulong sourceId)> StatusesToCancel = [];
public readonly List<(uint statusId, ulong sourceId)> StatusesToCancel = [];

// misc stuff to execute
public bool WantJump;
Expand Down
34 changes: 17 additions & 17 deletions BossMod/BossModule/BossModuleInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,21 +86,21 @@ public enum HuntRank : uint { B, A, S, SS }
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ModuleInfoAttribute(BossModuleInfo.Maturity maturity) : Attribute
{
public Type? StatesType { get; set; } // default: ns.xxxStates
public Type? ConfigType { get; set; } // default: ns.xxxConfig
public Type? ObjectIDType { get; set; } // default: ns.OID
public Type? ActionIDType { get; set; } // default: ns.AID
public Type? StatusIDType { get; set; } // default: ns.SID
public Type? TetherIDType { get; set; } // default: ns.TetherID
public Type? IconIDType { get; set; } // default: ns.IconID
public uint PrimaryActorOID { get; set; } // default: OID.Boss
public BossModuleInfo.Maturity Maturity { get; } = maturity;
public string Contributors { get; set; } = "";
public BossModuleInfo.Expansion Expansion { get; set; } = BossModuleInfo.Expansion.Count; // default: second namespace level
public BossModuleInfo.Category Category { get; set; } = BossModuleInfo.Category.Count; // default: third namespace level
public BossModuleInfo.GroupType GroupType { get; set; } = BossModuleInfo.GroupType.None;
public uint GroupID { get; set; }
public uint NameID { get; set; } // usually BNpcName row, unless GroupType uses it differently
public int SortOrder { get; set; } // default: first number in type name
public int PlanLevel { get; set; } // if > 0, module supports plans for this level
public Type? StatesType; // default: ns.xxxStates
public Type? ConfigType; // default: ns.xxxConfig
public Type? ObjectIDType; // default: ns.OID
public Type? ActionIDType; // default: ns.AID
public Type? StatusIDType; // default: ns.SID
public Type? TetherIDType; // default: ns.TetherID
public Type? IconIDType; // default: ns.IconID
public uint PrimaryActorOID; // default: OID.Boss
public BossModuleInfo.Maturity Maturity = maturity;
public string Contributors = "";
public BossModuleInfo.Expansion Expansion = BossModuleInfo.Expansion.Count; // default: second namespace level
public BossModuleInfo.Category Category = BossModuleInfo.Category.Count; // default: third namespace level
public BossModuleInfo.GroupType GroupType = BossModuleInfo.GroupType.None;
public uint GroupID;
public uint NameID; // usually BNpcName row, unless GroupType uses it differently
public int SortOrder; // default: first number in type name
public int PlanLevel; // if > 0, module supports plans for this level
}
2 changes: 1 addition & 1 deletion BossMod/BossModule/StateMachineTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class Node
public bool IsVulnerable;
public StateMachine.State State;
public Node? Predecessor;
public List<Node> Successors = [];
public readonly List<Node> Successors = [];

internal Node(float t, int phaseID, int branchID, StateMachine.State state, StateMachine.Phase phase, Node? pred)
{
Expand Down
79 changes: 63 additions & 16 deletions BossMod/Config/ConfigNode.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Reflection;

namespace BossMod;

// attribute that specifies how config node should be shown in the UI
[AttributeUsage(AttributeTargets.Class)]
public sealed class ConfigDisplayAttribute : Attribute
{
public string? Name { get; set; }
public int Order { get; set; }
public Type? Parent { get; set; }
public string? Name;
public int Order;
public Type? Parent;
}

// attribute that specifies how config node field or enumeration value is shown in the UI
[AttributeUsage(AttributeTargets.Field)]
public sealed class PropertyDisplayAttribute(string label, uint color = 0, string tooltip = "", bool separator = false) : Attribute
{
public string Label { get; } = label;
public uint Color { get; } = color == 0 ? Colors.TextColor1 : color;
public string Tooltip { get; } = tooltip;
public bool Separator { get; } = separator;
public string Label = label;
public uint Color = color == 0 ? Colors.TextColor1 : color;
public string Tooltip = tooltip;
public bool Separator = separator;
}

// attribute that specifies combobox should be used for displaying int/bool property
[AttributeUsage(AttributeTargets.Field)]
public sealed class PropertyComboAttribute(string[] values) : Attribute
{
public string[] Values { get; } = values;
public string[] Values = values;

#pragma warning disable CA1019 // this is just a shorthand
public PropertyComboAttribute(string falseText, string trueText) : this([falseText, trueText]) { }
Expand All @@ -37,10 +38,10 @@ public PropertyComboAttribute(string falseText, string trueText) : this([falseTe
[AttributeUsage(AttributeTargets.Field)]
public sealed class PropertySliderAttribute(float min, float max) : Attribute
{
public float Speed { get; set; } = 1;
public float Min { get; } = min;
public float Max { get; } = max;
public bool Logarithmic { get; set; }
public float Speed = 1;
public float Min = min;
public float Max = max;
public bool Logarithmic;
}

// base class for configuration nodes
Expand All @@ -54,15 +55,42 @@ public abstract class ConfigNode
// draw custom contents; override this for complex config nodes
public virtual void DrawCustom(UITree tree, WorldState ws) { }

private static readonly Dictionary<Type, FieldInfo[]> _fieldsCache = [];

private static FieldInfo[] GetSerializableFields(Type t)
{
if (_fieldsCache.TryGetValue(t, out var cachedFields))
return cachedFields;

var fields = t.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
var len = fields.Length;
var discoveredFields = new List<FieldInfo>(len);
for (var i = 0; i < len; ++i)
{
var field = fields[i];
if (!field.IsStatic && !field.IsDefined(typeof(JsonIgnoreAttribute), false))
{
discoveredFields.Add(field);
}
}

return _fieldsCache[t] = [.. discoveredFields];
}

// deserialize fields from json; default implementation should work fine for most cases
public virtual void Deserialize(JsonElement j, JsonSerializerOptions ser)
{
var type = GetType();
foreach (var jfield in j.EnumerateObject())
{
var field = type.GetField(jfield.Name);
var field = type.GetField(jfield.Name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field != null)
{
if (field.IsStatic)
continue;
if (field.GetCustomAttribute<JsonIgnoreAttribute>() != null)
continue;

var value = jfield.Value.Deserialize(field.FieldType, ser);
if (value != null)
{
Expand All @@ -72,10 +100,29 @@ public virtual void Deserialize(JsonElement j, JsonSerializerOptions ser)
}
}

// serialize node to json; default implementation should work fine for most cases
public virtual void Serialize(Utf8JsonWriter jwriter, JsonSerializerOptions ser)
// serialize node to json;
public virtual void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options)
{
JsonSerializer.Serialize(jwriter, this, GetType(), ser);
writer.WriteStartObject();

var fields = GetSerializableFields(GetType());
for (var i = 0; i < fields.Length; ++i)
{
var field = fields[i];
var fieldValue = field.GetValue(this);

writer.WritePropertyName(field.Name);
if (fieldValue is ConfigNode subNode)
{
subNode.Serialize(writer, options);
}
else
{
JsonSerializer.Serialize(writer, fieldValue, field.FieldType, options);
}
}

writer.WriteEndObject();
}
}

Expand Down