From b9263a12c5decaac48c670fe5172214f598e5d7c Mon Sep 17 00:00:00 2001 From: Matt Draper Date: Fri, 20 Mar 2026 12:37:51 +0000 Subject: [PATCH 1/3] Generate change detection factory for schemas with leveled bitfields For each schema with bitfield points that have level annotations, emit a static factory method that returns a BitfieldChangeDetector configured with the appropriate property getters and GetLevel calls. The runtime change detection logic lives in the consuming project. --- .../Generation/ModspecModelGenerator.cs | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/Modspec.Model/Generation/ModspecModelGenerator.cs b/Modspec.Model/Generation/ModspecModelGenerator.cs index 913106f..0908c65 100644 --- a/Modspec.Model/Generation/ModspecModelGenerator.cs +++ b/Modspec.Model/Generation/ModspecModelGenerator.cs @@ -77,7 +77,8 @@ namespace {schema.Name}; List bufferInitialisers = []; List fieldInitialisers = []; List constructorParams = []; - WriteGroups(schema.Groups, mainWriter, appendixWriter, bufferInitialisers, fieldInitialisers, constructorParams); + List bitfieldPointsWithLevels = []; + WriteGroups(schema.Groups, mainWriter, appendixWriter, bufferInitialisers, fieldInitialisers, constructorParams, bitfieldPointsWithLevels: bitfieldPointsWithLevels); foreach (RepeatingGroup repeatingGroup in schema.RepeatingGroups) { @@ -137,6 +138,11 @@ namespace {schema.Name}; mainWriter.WriteLine("}"); mainWriter.WriteLine(); + if (bitfieldPointsWithLevels.Count > 0) + { + WriteChangeDetectionFactory(schema.Name, bitfieldPointsWithLevels, appendixWriter); + } + result = mainWriter.ToString() + appendixWriter.ToString(); return true; } @@ -168,7 +174,7 @@ private static void WriteFieldsAndConstructor(string name, StringWriter mainWrit mainWriter.WriteLine($"{indent}\t}}"); } - private static void WriteGroups(IReadOnlyCollection groups, StringWriter mainWriter, StringWriter appendixWriter, List bufferInitialisers, List fieldInitialisers, List constructorParams, string indent = "", string readOffsetField = "") + private static void WriteGroups(IReadOnlyCollection groups, StringWriter mainWriter, StringWriter appendixWriter, List bufferInitialisers, List fieldInitialisers, List constructorParams, string indent = "", string readOffsetField = "", List? bitfieldPointsWithLevels = null) { foreach (Group group in groups) { @@ -185,7 +191,7 @@ private static void WriteGroups(IReadOnlyCollection groups, StringWriter // supplied count of elements, rather than max size of array) throw new InvalidOperationException($"An array must be the last (or only) element in a group."); } - WritePoint(point, bufferName, group.Table, mainWriter, appendixWriter, fieldInitialisers, constructorParams, ref maxOffset, ref bufferSize, indent); + WritePoint(point, bufferName, group.Table, mainWriter, appendixWriter, fieldInitialisers, constructorParams, ref maxOffset, ref bufferSize, indent, bitfieldPointsWithLevels); } if (String.IsNullOrEmpty(bufferSize)) { @@ -209,7 +215,7 @@ private static void WriteGroups(IReadOnlyCollection groups, StringWriter } } - private static void WritePoint(Point point, string bufferName, Table table, StringWriter mainWriter, StringWriter appendixWriter, List fieldInitialisers, List constructorParams, ref int maxOffset, ref string bufferSize, string indent = "") + private static void WritePoint(Point point, string bufferName, Table table, StringWriter mainWriter, StringWriter appendixWriter, List fieldInitialisers, List constructorParams, ref int maxOffset, ref string bufferSize, string indent = "", List? bitfieldPointsWithLevels = null) { string type; string readMethod; @@ -337,6 +343,7 @@ private static void WritePoint(Point point, string bufferName, Table table, Stri appendixWriter.WriteLine(); if (isFlags && masksByLevel.Count > 0) { + bitfieldPointsWithLevels?.Add(point.Name); appendixWriter.WriteLine($"public static class {point.Name}Extensions"); appendixWriter.WriteLine("{"); appendixWriter.WriteLine($"\tpublic static Level GetLevel(this {point.Name} self)"); @@ -421,6 +428,24 @@ private static void WritePoint(Point point, string bufferName, Table table, Stri maxOffset += point.SizeInBytes * (point.Count?.MaxValue ?? 1); } + private static void WriteChangeDetectionFactory(string schemaName, List points, StringWriter writer) + { + string clientName = $"{schemaName}Client"; + writer.WriteLine($"public static class {schemaName}ChangeDetection"); + writer.WriteLine("{"); + writer.WriteLine($"\tpublic static BitfieldChangeDetector<{clientName}> CreateDetector()"); + writer.WriteLine("\t{"); + writer.WriteLine($"\t\treturn new BitfieldChangeDetector<{clientName}>()"); + for (int i = 0; i < points.Count; i++) + { + string terminator = i < points.Count - 1 ? "" : ";"; + writer.WriteLine($"\t\t\t.Track(c => c.{points[i]}, v => v.GetLevel()){terminator}"); + } + writer.WriteLine("\t}"); + writer.WriteLine("}"); + writer.WriteLine(); + } + private static string ToFieldName(string name) { Span result = stackalloc char[name.Length + 1]; From 77bd959e9a8491b7052bc4600e5d95299589cede Mon Sep 17 00:00:00 2001 From: Matt Draper Date: Fri, 20 Mar 2026 16:13:28 +0000 Subject: [PATCH 2/3] Add BitfieldChangeDetector stub for test project compilation The source generator now emits a change detection factory that references BitfieldChangeDetector, which lives in the consuming project. The test project needs a minimal stub so the generated code compiles. --- Modspec.Test/BitfieldChangeDetectorStub.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 Modspec.Test/BitfieldChangeDetectorStub.cs diff --git a/Modspec.Test/BitfieldChangeDetectorStub.cs b/Modspec.Test/BitfieldChangeDetectorStub.cs new file mode 100644 index 0000000..ff854a2 --- /dev/null +++ b/Modspec.Test/BitfieldChangeDetectorStub.cs @@ -0,0 +1,13 @@ +/* + * Stub so the generated change detection factory compiles in the test project, + * which does not reference the real BitfieldChangeDetector implementation. + */ +namespace Modspec.Model; + +public class BitfieldChangeDetector +{ + public BitfieldChangeDetector Track(System.Func getter, System.Func getLevel) where T : struct, System.Enum + { + return this; + } +} From 147d0c85eddf7e56e334e2da5829d6376fdf43d0 Mon Sep 17 00:00:00 2001 From: Matt Draper Date: Tue, 24 Mar 2026 16:59:47 +0000 Subject: [PATCH 3/3] creates a feature flag for the detectionfactory --- Modspec.Model/Generation/ModspecModelGenerator.cs | 2 +- Modspec.Model/Schema.cs | 13 +++++++++++++ Modspec.Test/BitfieldChangeDetectorStub.cs | 13 ------------- 3 files changed, 14 insertions(+), 14 deletions(-) delete mode 100644 Modspec.Test/BitfieldChangeDetectorStub.cs diff --git a/Modspec.Model/Generation/ModspecModelGenerator.cs b/Modspec.Model/Generation/ModspecModelGenerator.cs index 0908c65..f26cdd9 100644 --- a/Modspec.Model/Generation/ModspecModelGenerator.cs +++ b/Modspec.Model/Generation/ModspecModelGenerator.cs @@ -138,7 +138,7 @@ namespace {schema.Name}; mainWriter.WriteLine("}"); mainWriter.WriteLine(); - if (bitfieldPointsWithLevels.Count > 0) + if (schema.GenerateChangeDetectionFactory && bitfieldPointsWithLevels.Count > 0) { WriteChangeDetectionFactory(schema.Name, bitfieldPointsWithLevels, appendixWriter); } diff --git a/Modspec.Model/Schema.cs b/Modspec.Model/Schema.cs index 936818f..48502b2 100644 --- a/Modspec.Model/Schema.cs +++ b/Modspec.Model/Schema.cs @@ -48,6 +48,19 @@ public class Schema /// public List RepeatingGroups { get; set; } = []; + /// + /// If true, the generator will emit a static factory class for creating a + /// BitfieldChangeDetector that tracks all bitfield points with level annotations. + /// The consuming project must provide a BitfieldChangeDetector<TClient> class + /// in the Modspec.Model namespace with the following method: + /// + /// BitfieldChangeDetector<TClient> Track<T>(Func<TClient, T> getter, Func<T, Level> getLevel) + /// where T : struct, Enum + /// + /// The Track method must return the detector instance to support fluent chaining. + /// + public bool GenerateChangeDetectionFactory { get; set; } + public void Serialise(Stream stream) { JsonSerializer.Serialize(stream, this, Options); diff --git a/Modspec.Test/BitfieldChangeDetectorStub.cs b/Modspec.Test/BitfieldChangeDetectorStub.cs deleted file mode 100644 index ff854a2..0000000 --- a/Modspec.Test/BitfieldChangeDetectorStub.cs +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Stub so the generated change detection factory compiles in the test project, - * which does not reference the real BitfieldChangeDetector implementation. - */ -namespace Modspec.Model; - -public class BitfieldChangeDetector -{ - public BitfieldChangeDetector Track(System.Func getter, System.Func getLevel) where T : struct, System.Enum - { - return this; - } -}