Skip to content

feat: Nullable<T> fields, null-safe collection helpers, migration timer fix (v4.2.0) - #60

Merged
kamronbatman merged 1 commit into
mainfrom
kb/migration-timer-absent-branch
Sep 24, 2026
Merged

kamronbatman merged 1 commit into
mainfrom
kb/migration-timer-absent-branch

Conversation

@kamronbatman

@kamronbatman kamronbatman commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Problems

  1. Migration content struct breaks on an absent save-flagged Timer. For an absent flag, ContentStruct.cs emitted {property.Name} = default; itself. TimerMigrationRule declares {Name}Next / {Name}Delay on the struct, not {Name}, so the line bound to the entity's instance member (CS0120) or to nothing (CS0103). Found while bumping ModernUO's BaseCreature from v23 to v24 (PendingDeleteTimer).
  2. Nullable reference annotations crash the generator. string?, Dictionary<int, string?> or string?[] reached the rules as "string?". PrimitiveTypeMigrationRule threw SG0001 (InvalidOperationException).
  3. No Nullable<T> support. int?, Point3D? or a nullable generated struct failed with SG3007 (no migration rule).
  4. Collection helpers threw on null collections and always marked dirty. AddToX / RemoveFromX / ClearX called straight through the property. They threw NullReferenceException on a lazily-null collection and called MarkDirty even when nothing changed.

Changes

  • Absent-flag assignment is owned by the rule. New ISerializableMigrationRule.GenerateMigrationAbsentAssignment. MigrationRule keeps {Name} = default;, so other rules' output is byte-identical. TimerMigrationRule emits Next = DateTime.MinValue; Delay = TimeSpan.MinValue;, the same "no timer was running" values the present branch produces.
  • ToSerializedTypeName() drops reference-type ? and keeps Nullable<T> as int?. The rules engine and every collection/KVP rule use it for type names and rule arguments.
  • NullableMigrationRule, registered first. It writes a HasValue bool, then the value through T's own rule, so it composes with enums, UO types, [EncodedInt], generated structs, list elements and dictionary values. Schema: "rule": "NullableMigrationRule", "ruleArguments": [T, T's rule, ...T's arguments].
  • Collection helpers now mutate the backing field. Going through the setter would run fieldChanged, and allowFieldChange could veto the new collection.
    • AddToX / InsertIntoX / ReplaceInX create the collection when null, keeping [SortedSetComparer]. Types without an accessible parameterless ctor keep the old behavior.
    • RemoveFromX / RemoveFromXAt / ClearX do nothing when the collection is null.
    • MarkDirty only fires on an actual change: set Add returns true, dictionary TryAdd (or ContainsKey + Add for other IDictionary types), Remove returns true, Clear on a non-empty collection.
  • Generator, SchemaGenerator and Annotations bumped to 4.2.0. README install snippet updated, plus a short section on helpers and nullable fields.

Behavior changes for callers

  • AddToX on a dictionary no longer throws on a duplicate key; it does nothing.
  • ClearX on an empty collection no longer marks dirty.
  • The helpers bypass a virtual property override.

Tests

  • Snapshots/MigrationSaveFlagTimer: save-flagged anchored, wall-clock and legacy drift timers in a v0 schema.
  • MigrationSaveFlagTests.MigrateFrom_SeesTimerSentinelsWhenTheSaveFlagIsAbsent: writes v0 with the live generator (timers running or absent, anchored and wall-clock), then migrates. It asserts a positive Delay when running, MinValue sentinels when absent, and that the stream stays aligned.
  • Snapshots/NullableCollections and DataStructureMethodTests: nullable-annotated fields, plus a runtime run of every helper, on null and populated collections, checking the MarkDirty count after each step.
  • Snapshots/NullableValueTypes and NullableValueTypeTests: a live round trip of every Nullable<T> shape, populated and null. Also a v0 to v1 migration whose schema comes from schema mode via a new GenerateMigrationSchemas helper, not hand-written JSON.
  • Red without the fix: the timer snapshot and round trip fail on main with CS0103, and the nullable fixtures fail on main with SG0001 / SG3007.
  • Existing snapshots: only helper-method output changed (7 files). All 129 tests pass.

After merge

Run the Build and Publish NuGet Packages workflow. Then ModernUO #2670 can bump to 4.2.0 and regenerate BaseCreature.v24.json.

…er fix (v4.2.0)

- Fix: a migration content struct with a save-flagged Timer no longer emits
  `{Name} = default;` for an absent flag (CS0120/CS0103). Rules now own the
  absent assignment (ISerializableMigrationRule.GenerateMigrationAbsentAssignment);
  TimerMigrationRule sets Next/Delay to DateTime.MinValue/TimeSpan.MinValue.
- Fix: nullable reference annotations (string?, Dictionary<int, string?>,
  string?[]) crashed the generator (SG0001). Serialized type names now drop
  reference-type `?` and keep Nullable<T>.
- Feature: NullableMigrationRule serializes Nullable<T> fields (int?, enums,
  Point3D?, [SerializationGenerator] structs, and nested in collections) as a
  HasValue bool followed by the value through T's own rule.
- Collection helpers mutate the backing field, lazily create a null collection
  on add/insert/replace (keeping [SortedSetComparer]), tolerate null on
  remove/clear, and mark dirty only on an actual change (set Add, dictionary
  TryAdd, Remove results, non-empty Clear).
- Bump Generator, SchemaGenerator and Annotations to 4.2.0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@kamronbatman
kamronbatman merged commit d8dad65 into main Sep 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant