diff --git a/Projects/UOContent.Tests/Tests/Commands/Objects/ObjectIntrospectionOplTests.cs b/Projects/UOContent.Tests/Tests/Commands/Objects/ObjectIntrospectionOplTests.cs new file mode 100644 index 000000000..b360a2726 --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Commands/Objects/ObjectIntrospectionOplTests.cs @@ -0,0 +1,17 @@ +using Server.Commands; +using Server.Items; +using Xunit; + +namespace UOContent.Tests.Commands.Objects; + +[Collection("Sequential UOContent Tests")] +public class ObjectIntrospectionOplTests +{ + [Fact] + public void ExtractOpl_captures_the_runebook_name_cliloc() + { + // Runebook.LabelNumber is 1041267 ("runebook") — it appears as an OPL line. + var opl = ObjectIntrospection.ExtractOpl(typeof(Runebook)); + Assert.Contains(opl, line => line.Cliloc == 1041267); + } +} diff --git a/Projects/UOContent/Commands/Object Creation/ObjectIntrospection.cs b/Projects/UOContent/Commands/Object Creation/ObjectIntrospection.cs index f264a76f0..f4d076f97 100644 --- a/Projects/UOContent/Commands/Object Creation/ObjectIntrospection.cs +++ b/Projects/UOContent/Commands/Object Creation/ObjectIntrospection.cs @@ -1,6 +1,8 @@ using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.Reflection; +using System.Text; using Server.Items; namespace Server.Commands; @@ -120,4 +122,66 @@ public static class ObjectIntrospection return docs; } + + public static List ExtractOpl(Type type) + { + if (type.IsAssignableTo(typeof(Item))) + { + var item = type.CreateInstance(); + try + { + var opl = new ObjectPropertyList(item); + item.GetProperties(opl); + return DecodeOpl(opl); + } + finally + { + item.Delete(); + } + } + + if (type.IsAssignableTo(typeof(Mobile))) + { + var m = type.CreateInstance(); + try + { + var opl = new ObjectPropertyList(m); + m.GetProperties(opl); + return DecodeOpl(opl); + } + finally + { + m.Delete(); + } + } + + return []; + } + + private static List DecodeOpl(ObjectPropertyList opl) + { + opl.Terminate(); + var buffer = opl.Buffer; + var lines = new List(); + var pos = 15; // fixed OPL header length + + while (pos + 4 <= buffer.Length) + { + var cliloc = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(pos)); + pos += 4; + if (cliloc == 0) + { + break; + } + + var byteLen = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(pos)); + pos += 2; + var args = byteLen > 0 ? Encoding.Unicode.GetString(buffer, pos, byteLen) : null; + pos += byteLen; + + lines.Add(new OplLine { Cliloc = cliloc, Args = args, Text = Localization.GetText(cliloc) }); + } + + return lines; + } }