using ACE.Database.Models.World;
using System.Globalization;
namespace Aeshnidae.DevKit;
///
/// One-line renderings of the parts of a weenie a content developer asks about, in the
/// terms they think in: "Give tusk -> AwardXP 95,000,000; Tell ..." rather than a row
/// in weenie_properties_emote_action with type = 2 and amount_64 = 95000000.
///
/// Used by /winfo to describe a weenie and by /wdiff to describe what changed, so the
/// two read the same.
///
internal static class Format
{
private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;
// ---- emotes --------------------------------------------------------------
/// The emote's trigger: category, what item or text triggers it, its odds.
public static string EmoteHeader(WeeniePropertiesEmote e)
{
var sb = new StringBuilder();
sb.Append(EnumName(e.Category));
if (e.WeenieClassId is not null)
sb.Append(' ').Append('<').Append(Names.WeenieRef(e.WeenieClassId)).Append('>');
if (!string.IsNullOrEmpty(e.Quest))
sb.Append(" \"").Append(e.Quest).Append('"');
if (e.VendorType is not null)
sb.Append(" vendorType=").Append(e.VendorType);
if (e.Style is not null || e.Substyle is not null)
sb.Append(" style=").Append(Hex(e.Style)).Append('/').Append(Hex(e.Substyle));
if (e.MinHealth is not null || e.MaxHealth is not null)
sb.Append(" health=").Append(e.MinHealth?.ToString(Inv) ?? "").Append("..").Append(e.MaxHealth?.ToString(Inv) ?? "");
if (Math.Abs(e.Probability - 1f) > 0.0001f)
sb.Append(" p=").Append(e.Probability.ToString("0.###", Inv));
return sb.ToString();
}
/// Trigger plus every action, on one line.
public static string Emote(WeeniePropertiesEmote e)
{
var actions = e.WeeniePropertiesEmoteAction?.OrderBy(a => a.Order).Select(Action) ?? Enumerable.Empty();
return $"{EmoteHeader(e)}: {string.Join(" ; ", actions)}";
}
public static string Action(WeeniePropertiesEmoteAction a)
{
var type = (EmoteType)a.Type;
var name = Enum.IsDefined(type) ? type.ToString() : $"type{a.Type}";
switch (type)
{
case EmoteType.AwardXP:
case EmoteType.AwardNoShareXP:
case EmoteType.AwardLuminance:
case EmoteType.SpendLuminance:
return $"{name} {N(a.Amount64 ?? a.Amount)}" + (a.HeroXP64 is > 0 ? $" (hero {N(a.HeroXP64)})" : "");
case EmoteType.AwardLevelProportionalXP:
return $"{name} {Pct(a.Percent)} of level, {N(a.Min64)}..{N(a.Max64)}" + (a.Display == true ? " (shown)" : "");
case EmoteType.AwardSkillXP:
case EmoteType.AwardSkillPoints:
return $"{name} {SkillName(a.Stat)} {N(a.Amount)}";
case EmoteType.Tell:
case EmoteType.Say:
case EmoteType.TextDirect:
case EmoteType.DirectBroadcast:
case EmoteType.LocalBroadcast:
case EmoteType.WorldBroadcast:
case EmoteType.TellFellow:
case EmoteType.PopUp:
return $"{name} \"{Clip(a.Message, 90)}\"";
case EmoteType.Give:
case EmoteType.TakeItems:
return $"{name} {(a.StackSize is > 1 ? a.StackSize + "x " : "")}<{Names.WeenieRef(a.WeenieClassId)}>";
case EmoteType.CastSpell:
case EmoteType.CastSpellInstant:
case EmoteType.PetCastSpellOnOwner:
return $"{name} {Names.Spell(a.SpellId)}";
case EmoteType.CreateTreasure:
return $"{name} wealth={a.WealthRating} class={a.TreasureClass} type={a.TreasureType}";
case EmoteType.Motion:
case EmoteType.ForceMotion:
return $"{name} 0x{a.Motion:X}";
case EmoteType.Goto:
return $"{name} \"{a.Message}\"";
case EmoteType.Sound:
return $"{name} {a.Sound}";
case EmoteType.PhysScript:
return $"{name} {a.PScript}";
}
// Quest actions carry the quest name in Message; stat inquiries carry a stat
// and a range. Anything else prints whichever fields it has set.
var sb = new StringBuilder(name);
if (!string.IsNullOrEmpty(a.Message))
sb.Append(name.StartsWith("Inq") || name.Contains("Quest") || name.Contains("Event") ? " " : " \"").Append(Clip(a.Message, 60)).Append(name.StartsWith("Inq") || name.Contains("Quest") || name.Contains("Event") ? "" : "\"");
if (a.Stat is not null)
sb.Append(" stat=").Append(StatName(type, a.Stat.Value));
if (a.Min is not null || a.Max is not null)
sb.Append(' ').Append(a.Min?.ToString(Inv) ?? "").Append("..").Append(a.Max?.ToString(Inv) ?? "");
if (a.Min64 is not null || a.Max64 is not null)
sb.Append(' ').Append(N(a.Min64)).Append("..").Append(N(a.Max64));
if (a.MinDbl is not null || a.MaxDbl is not null)
sb.Append(' ').Append(a.MinDbl?.ToString("0.###", Inv) ?? "").Append("..").Append(a.MaxDbl?.ToString("0.###", Inv) ?? "");
if (a.Amount is not null)
sb.Append(" amount=").Append(N(a.Amount));
if (a.Amount64 is not null)
sb.Append(" amount64=").Append(N(a.Amount64));
if (a.Percent is not null)
sb.Append(' ').Append(Pct(a.Percent));
if (a.WeenieClassId is not null)
sb.Append(" <").Append(Names.WeenieRef(a.WeenieClassId)).Append('>');
if (a.SpellId is not null)
sb.Append(' ').Append(Names.Spell(a.SpellId));
if (!string.IsNullOrEmpty(a.TestString))
sb.Append(" test=\"").Append(Clip(a.TestString, 40)).Append('"');
if (a.Delay > 0)
sb.Append(" delay=").Append(a.Delay.ToString("0.##", Inv));
return sb.ToString();
}
// ---- other tables ----------------------------------------------------------
public static string CreateList(WeeniePropertiesCreateList c)
{
var dest = EnumName((uint)c.DestinationType);
var qty = c.StackSize is > 1 or < 0 ? $"{c.StackSize}x " : "";
var extra = "";
if (c.Palette != 0) extra += $" palette={c.Palette}";
if (c.Shade != 0) extra += $" shade={c.Shade.ToString("0.##", Inv)}";
if (c.TryToBond) extra += " bond";
return $"{dest} {qty}<{Names.WeenieRef(c.WeenieClassId)}>{extra}";
}
public static string Generator(WeeniePropertiesGenerator g)
{
var where = EnumName(g.WhereCreate);
var when = EnumName(g.WhenCreate);
var s = $"<{Names.WeenieRef(g.WeenieClassId)}> init={g.InitCreate} max={g.MaxCreate} p={g.Probability.ToString("0.###", Inv)} {when}/{where}";
if (g.Delay is not null) s += $" delay={g.Delay.Value.ToString("0.#", Inv)}s";
if (g.StackSize is not null) s += $" stack={g.StackSize}";
if (g.OriginX is not null) s += $" @({g.OriginX?.ToString("0.##", Inv)}, {g.OriginY?.ToString("0.##", Inv)}, {g.OriginZ?.ToString("0.##", Inv)})";
return s;
}
public static string SpellBook(WeeniePropertiesSpellBook s) =>
Math.Abs(s.Probability - 2f) < 0.0001f || s.Probability >= 1f
? Names.Spell(s.Spell)
: $"{Names.Spell(s.Spell)} p={s.Probability.ToString("0.###", Inv)}";
public static string Instance(LandblockInstance i) =>
$"0x{i.Guid:X8} <{Names.WeenieRef(i.WeenieClassId)}> cell 0x{i.ObjCellId:X8} @({i.OriginX.ToString("0.###", Inv)}, {i.OriginY.ToString("0.###", Inv)}, {i.OriginZ.ToString("0.###", Inv)})" +
$" rot({i.AnglesW.ToString("0.###", Inv)}, {i.AnglesX.ToString("0.###", Inv)}, {i.AnglesY.ToString("0.###", Inv)}, {i.AnglesZ.ToString("0.###", Inv)})" +
(i.IsLinkChild ? " child" : "");
/// "Level" for (int, 25); "Name" for (string, 1) - the property's enum name.
public static string PropertyName(string kind, uint type) => kind switch
{
"int" => EnumName(type),
"int64" => EnumName(type),
"bool" => EnumName(type),
"float" => EnumName(type),
"string" => EnumName(type),
"d_i_d" => EnumName(type),
"i_i_d" => EnumName(type),
"position" => EnumName(type),
"attribute" => EnumName(type),
"attribute_2nd" => EnumName(type),
"skill" => EnumName(type),
_ => type.ToString(),
};
/// Value rendered for its property: enums decoded where ACE has one, ids in hex.
public static string PropertyValue(string kind, uint type, object? value)
{
if (value is null) return "null";
if (kind == "int")
{
var v = Convert.ToInt32(value);
return (PropertyInt)type switch
{
PropertyInt.ItemType => $"{EnumName((uint)v)} ({v})",
PropertyInt.CreatureType => $"{EnumName((uint)v)} ({v})",
PropertyInt.PlayerKillerStatus => $"{EnumName((uint)v)} ({v})",
PropertyInt.RadarBlipColor => $"{EnumName((uint)v)} ({v})",
PropertyInt.ItemUseable => $"{EnumName((uint)v)} ({v})",
PropertyInt.MerchandiseItemTypes => $"0x{v:X}",
PropertyInt.PortalBitmask => $"{EnumName((uint)v)} ({v})",
PropertyInt.AmmoType => $"{EnumName((uint)v)} ({v})",
PropertyInt.DamageType => $"{EnumName((uint)v)} ({v})",
PropertyInt.CombatUse => $"{EnumName((uint)v)} ({v})",
PropertyInt.ValidLocations or PropertyInt.CurrentWieldedLocation => $"{EnumName((uint)v)} ({v})",
PropertyInt.WieldSkillType => $"{EnumName((uint)v)} ({v})",
PropertyInt.WieldRequirements => $"{EnumName((uint)v)} ({v})",
_ => N(v),
};
}
if (kind == "int64") return N(Convert.ToInt64(value));
if (kind == "d_i_d" || kind == "i_i_d") return $"0x{Convert.ToUInt32(value):X8}";
if (kind == "bool") return Convert.ToBoolean(value) ? "True" : "False";
if (kind == "float") return Convert.ToDouble(value).ToString("0.####", Inv);
if (kind == "string") return $"\"{value}\"";
return Convert.ToString(value, Inv) ?? "";
}
// ---- helpers ----------------------------------------------------------------
public static string EnumName(uint value) where T : struct, Enum
{
var t = (T)Enum.ToObject(typeof(T), value);
return Enum.IsDefined(t) || typeof(T).IsDefined(typeof(FlagsAttribute), false) ? t.ToString() : $"{typeof(T).Name}{value}";
}
private static string SkillName(int? stat) => stat is null ? "" : EnumName((uint)stat.Value);
private static string StatName(EmoteType type, int stat)
{
var n = type.ToString();
if (n.Contains("Int64")) return EnumName((uint)stat);
if (n.Contains("Int")) return EnumName((uint)stat);
if (n.Contains("Bool")) return EnumName((uint)stat);
if (n.Contains("Float")) return EnumName((uint)stat);
if (n.Contains("String")) return EnumName((uint)stat);
if (n.Contains("Skill")) return EnumName((uint)stat);
if (n.Contains("SecondaryAttribute")) return EnumName((uint)stat);
if (n.Contains("Attribute")) return EnumName((uint)stat);
return stat.ToString(Inv);
}
public static string N(long? v) => v is null ? "" : v.Value.ToString("#,0", Inv);
public static string N(int? v) => v is null ? "" : v.Value.ToString("#,0", Inv);
private static string Pct(double? p) => p is null ? "" : (p.Value * 100).ToString("0.##", Inv) + "%";
private static string Hex(uint? v) => v is null ? "" : $"0x{v:X}";
public static string Clip(string? s, int max)
{
if (string.IsNullOrEmpty(s)) return "";
s = s.Replace("\r", "").Replace("\n", " / ");
return s.Length <= max ? s : s[..max] + "…";
}
/// Wrap "a, b, c, ..." lines at roughly the width the chat window shows without scrolling.
public static IEnumerable Wrap(string prefix, IEnumerable items, int width = 170)
{
var line = new StringBuilder(prefix);
var first = true;
foreach (var item in items)
{
if (!first && line.Length + item.Length + 2 > width)
{
yield return line.ToString();
line.Clear().Append(new string(' ', Math.Min(prefix.Length, 4)));
first = true;
}
if (!first) line.Append(", ");
line.Append(item);
first = false;
}
if (line.Length > 0 && !first)
yield return line.ToString();
}
}