Skip to content

Commit 91d6481

Browse files
committed
Merge branch 'dev'
2 parents 2fdae6f + 09449e9 commit 91d6481

File tree

166 files changed

+15655
-1730
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

166 files changed

+15655
-1730
lines changed

DEVELOPMENT.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,31 @@ cd Mutagen.Bethesda.Generator.All/bin/Debug/net8.0
137137

138138
The generators use relative paths like `../../../../Mutagen.Bethesda.{Game}/Records` to locate project files. Running from the build output directory ensures these relative paths resolve correctly to the repository structure.
139139

140+
## Project File Management
141+
142+
### Adding New Files to Projects
143+
144+
**Important**: Most projects in this repository use `<EnableDefaultCompileItems>False</EnableDefaultCompileItems>`, which means all source files must be explicitly listed in the `.csproj` file. This is necessary to properly nest generated code files under their corresponding XML definition files.
145+
146+
When adding new `.cs` files to a project:
147+
148+
1. **Find the correct location** in the `.csproj` file (files are typically grouped by directory/feature)
149+
2. **Add a `<Compile Include="...">` element** with the file path
150+
3. **For generated files only**: Add a `<DependentUpon>...</DependentUpon>` element to nest it under the XML file
151+
152+
Example:
153+
```xml
154+
<!-- Regular file (no nesting) -->
155+
<Compile Include="Plugins\Analysis\DI\MultiModFileReader.cs" />
156+
157+
<!-- Generated file (nested under XML) -->
158+
<Compile Include="Records\SkyrimMod_Generated.cs">
159+
<DependentUpon>SkyrimMod.xml</DependentUpon>
160+
</Compile>
161+
```
162+
163+
If you create a new file and the build can't find it, check that it's been added to the `.csproj` file.
164+
140165
## Development Workflow
141166

142167
### Always Verify Your Changes
@@ -156,6 +181,18 @@ dotnet test Mutagen.UnitTests.sln
156181

157182
This ensures your changes don't break the build or existing functionality.
158183

184+
### File System Operations
185+
- **NEVER redirect to `nul`** - On Windows, `2>nul` creates unwanted files that Git tracks
186+
- Use proper null redirection: `2>/dev/null` (works on Windows with bash)
187+
- For temporary files, use `.claude/` subfolder or designated temp directories that are gitignored
188+
- Example: `ls directory 2>/dev/null || echo "Not found"` instead of `dir directory 2>nul`
189+
- **NEVER use `sed` for bulk find/replace** - `sed` does not preserve Windows CRLF line endings, creating massive spurious diffs
190+
- On Windows, `sed -i` converts CRLF to LF, causing every line to show as changed in git
191+
- Use targeted edits with the Edit tool instead of global sed replacements
192+
- If you must do bulk replacements, only use tools that preserve line endings (e.g., PowerShell with `-Raw` and explicit encoding)
193+
- Example (incorrect): `find . -name "*.cs" -exec sed -i 's/OldName/NewName/g' {} \;` - creates CRLF→LF changes on every touched file
194+
- Example (correct): Use Edit tool on each file individually, or ask user to use IDE refactoring tools
195+
159196
## Releases
160197

161198
### Packaging
@@ -189,6 +226,35 @@ Mutagen uses AutoFixture with custom builders to automatically generate properly
189226
- If you want, you can use `[Theory, MutagenModAutoData]` which will allow injection of mods, and records that are added to the latest mod.
190227
- AutoFixture will inject properly configured `SkyrimMod`, `ModKey`, `FormKey`, etc. as test parameters
191228

229+
## Coding Practices
230+
231+
### Avoid Using `dynamic`
232+
233+
Do not use `dynamic` when possible. The codebase provides proper interfaces and generic methods that should be used instead. Using `dynamic` bypasses compile-time type checking and can lead to runtime errors.
234+
235+
### Prefer ModPath Over DirectoryPath + ModKey
236+
237+
When working with paths to mod files, prefer using `ModPath` instead of separate `DirectoryPath` and `ModKey` parameters. `ModPath` is essentially a string path that is expected to point to a mod file, and includes the associated `ModKey`.
238+
239+
```cs
240+
// Preferred - uses ModPath
241+
public void ProcessMod(ModPath modPath)
242+
{
243+
var modKey = modPath.ModKey;
244+
var filePath = modPath.Path;
245+
// ...
246+
}
247+
248+
// Avoid - separate parameters
249+
public void ProcessMod(DirectoryPath folder, ModKey modKey)
250+
{
251+
var filePath = Path.Combine(folder.Path, modKey.FileName);
252+
// ...
253+
}
254+
```
255+
256+
`ModPath` provides better API ergonomics and ensures the path and ModKey stay in sync.
257+
192258
## Contributing
193259

194260
See the main README.md and official documentation for contribution guidelines.

Directory.Packages.props

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
<PackageVersion Include="Microsoft.Reactive.Testing" Version="6.0.2" />
3434
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
3535
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
36-
<PackageVersion Include="Noggog.Autofac" Version="3.1.0" />
37-
<PackageVersion Include="Noggog.CSharpExt" Version="3.1.0" />
38-
<PackageVersion Include="Noggog.Testing" Version="3.1.0" />
39-
<PackageVersion Include="Noggog.WPF" Version="3.1.0" />
36+
<PackageVersion Include="Noggog.Autofac" Version="3.2.0-alpha.6" />
37+
<PackageVersion Include="Noggog.CSharpExt" Version="3.2.0-alpha.6" />
38+
<PackageVersion Include="Noggog.Testing" Version="3.2.0-alpha.6" />
39+
<PackageVersion Include="Noggog.WPF" Version="3.2.0-alpha.6" />
4040
<PackageVersion Include="ReactiveUI" Version="21.0.1" />
4141
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
4242
<PackageVersion Include="ReactiveMarbles.ObservableEvents.SourceGenerator" Version="1.3.1" PrivateAssets="all" />

Mutagen.Bethesda.Core.UnitTests/Json/JsonConverterTests.cs

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Loqui;
12
using Shouldly;
23
using Mutagen.Bethesda.Json;
34
using Mutagen.Bethesda.Plugins;
@@ -12,6 +13,12 @@ namespace Mutagen.Bethesda.UnitTests.Json;
1213

1314
public class JsonConverterTests
1415
{
16+
static JsonConverterTests()
17+
{
18+
Warmup.Init();
19+
LoquiRegistration.Register(TestMajorRecord_Registration.Instance);
20+
}
21+
1522
#region FormKey
1623
class FormKeyClass
1724
{
@@ -174,7 +181,7 @@ public void FormKeyConverter_FormLink_Serialize()
174181
Getter = new FormLink<ITestMajorRecordGetter>(TestConstants.Form2)
175182
};
176183
JsonConvert.SerializeObject(toSerialize, settings)
177-
.ShouldBe($"{{\"Direct\":\"{toSerialize.Direct.FormKey}\",\"Setter\":\"{toSerialize.Direct.FormKey}\",\"Getter\":\"{toSerialize.Direct.FormKey}\"}}");
184+
.ShouldBe($"{{\"Direct\":\"{toSerialize.Direct.FormKey}<TestGame.TestMajorRecord>\",\"Setter\":\"{toSerialize.Direct.FormKey}<TestGame.TestMajorRecord>\",\"Getter\":\"{toSerialize.Direct.FormKey}<TestGame.TestMajorRecord>\"}}");
178185
}
179186

180187
[Fact]
@@ -188,7 +195,7 @@ public void FormKeyConverter_FormLink_Deserialize()
188195
Setter = new FormLink<ITestMajorRecordGetter>(TestConstants.Form2),
189196
Getter = new FormLink<ITestMajorRecordGetter>(TestConstants.Form2)
190197
};
191-
var toDeserialize = $"{{\"Direct\":\"{target.Direct.FormKey}\",\"Setter\":\"{target.Direct.FormKey}\",\"Getter\":\"{target.Direct.FormKey}\"}}";
198+
var toDeserialize = $"{{\"Direct\":\"{target.Direct.FormKey}<TestGame.TestMajorRecord>\",\"Setter\":\"{target.Direct.FormKey}<TestGame.TestMajorRecord>\",\"Getter\":\"{target.Direct.FormKey}<TestGame.TestMajorRecord>\"}}";
192199
JsonConvert.DeserializeObject<FormLinkClass>(toDeserialize, settings)!
193200
.Direct
194201
.ShouldBe(target.Direct);
@@ -206,6 +213,21 @@ public void FormKeyConverter_FormLink_Deserialize_Missing()
206213
.ShouldBe(target.Direct);
207214
}
208215

216+
[Fact]
217+
public void FormKeyConverter_FormLink_Serialize_Null()
218+
{
219+
var settings = new JsonSerializerSettings();
220+
settings.Converters.Add(new FormKeyJsonConverter());
221+
var toSerialize = new FormLinkClass()
222+
{
223+
Direct = new FormLink<ITestMajorRecordGetter>(FormKey.Null),
224+
Setter = new FormLink<ITestMajorRecordGetter>(FormKey.Null),
225+
Getter = new FormLink<ITestMajorRecordGetter>(FormKey.Null)
226+
};
227+
JsonConvert.SerializeObject(toSerialize, settings)
228+
.ShouldBe($"{{\"Direct\":\"Null<TestGame.TestMajorRecord>\",\"Setter\":\"Null<TestGame.TestMajorRecord>\",\"Getter\":\"Null<TestGame.TestMajorRecord>\"}}");
229+
}
230+
209231
[Fact]
210232
public void FormKeyConverter_FormLink_Deserialize_Null()
211233
{
@@ -217,7 +239,7 @@ public void FormKeyConverter_FormLink_Deserialize_Null()
217239
Setter = new FormLink<ITestMajorRecordGetter>(FormKey.Null),
218240
Getter = new FormLink<ITestMajorRecordGetter>(FormKey.Null)
219241
};
220-
var toDeserialize = $"{{\"Direct\":\"Null\",\"Setter\":\"Null\",\"Getter\":\"Null\"}}";
242+
var toDeserialize = $"{{\"Direct\":\"Null<TestGame.TestMajorRecord>\",\"Setter\":\"Null<TestGame.TestMajorRecord>\",\"Getter\":\"Null<TestGame.TestMajorRecord>\"}}";
221243
JsonConvert.DeserializeObject<FormLinkClass>(toDeserialize, settings)!
222244
.Direct
223245
.ShouldBe(target.Direct);
@@ -283,7 +305,7 @@ public void FormKeyConverter_FormLink_Deserialize_Empty()
283305
// Member = new FormLink<ITestMajorRecordGetter>(FormKey.Null)
284306
// };
285307
// JsonConvert.SerializeObject(toSerialize, settings)
286-
// .ShouldBe($"{{\"Member\":\"Null\"}}");
308+
// .ShouldBe($"{{\"Member\":\"Null<TestGame.TestMajorRecord>\"}}");
287309
// }
288310
//
289311
// [Fact]
@@ -295,7 +317,7 @@ public void FormKeyConverter_FormLink_Deserialize_Empty()
295317
// {
296318
// Member = new FormLink<ITestMajorRecordGetter>(TestConstants.Form2)
297319
// };
298-
// var toDeserialize = $"{{\"Member\":\"{target.Member.FormKey}\"}}";
320+
// var toDeserialize = $"{{\"Member\":\"{target.Member.FormKey}<TestGame.TestMajorRecord>\"}}";
299321
// JsonConvert.DeserializeObject<NullableFormLinkClass>(toDeserialize, settings)!
300322
// .Member
301323
// .ShouldBe(target.Member);
@@ -329,7 +351,7 @@ public void FormKeyConverter_FormLink_Deserialize_Empty()
329351
// {
330352
// var settings = new JsonSerializerSettings();
331353
// settings.Converters.Add(new FormKeyJsonConverter());
332-
// var toDeserialize = $"{{\"Member\":\"Null\"}}";
354+
// var toDeserialize = $"{{\"Member\":\"Null<TestGame.TestMajorRecord>\"}}";
333355
// JsonConvert.DeserializeObject<NullableFormLinkClass>(toDeserialize, settings)!
334356
// .Member!.IsNull
335357
// .ShouldBeTrue();
@@ -352,7 +374,7 @@ public void FormKeyConverter_FormLinkNullable_Serialize()
352374
Member = new FormLinkNullable<ITestMajorRecordGetter>(TestConstants.Form2)
353375
};
354376
JsonConvert.SerializeObject(toSerialize, settings)
355-
.ShouldBe($"{{\"Member\":\"{toSerialize.Member.FormKey}\"}}");
377+
.ShouldBe($"{{\"Member\":\"{toSerialize.Member.FormKey}<TestGame.TestMajorRecord>\"}}");
356378
}
357379

358380
[Fact]
@@ -365,7 +387,7 @@ public void FormKeyConverter_FormLinkNullable_Serialize_Null()
365387
Member = new FormLinkNullable<ITestMajorRecordGetter>(default(FormKey?))
366388
};
367389
JsonConvert.SerializeObject(toSerialize, settings)
368-
.ShouldBe($"{{\"Member\":null}}");
390+
.ShouldBe($"{{\"Member\":\"Null<TestGame.TestMajorRecord>\"}}");
369391
}
370392

371393
[Fact]
@@ -378,7 +400,7 @@ public void FormKeyConverter_FormLinkNullable_Serialize_NullFormKey()
378400
Member = new FormLinkNullable<ITestMajorRecordGetter>(FormKey.Null)
379401
};
380402
JsonConvert.SerializeObject(toSerialize, settings)
381-
.ShouldBe($"{{\"Member\":\"Null\"}}");
403+
.ShouldBe($"{{\"Member\":\"Null<TestGame.TestMajorRecord>\"}}");
382404
}
383405

384406
[Fact]
@@ -390,7 +412,7 @@ public void FormKeyConverter_FormLinkNullable_Deserialize()
390412
{
391413
Member = new FormLinkNullable<ITestMajorRecordGetter>(TestConstants.Form2)
392414
};
393-
var toDeserialize = $"{{\"Member\":\"{target.Member.FormKey}\"}}";
415+
var toDeserialize = $"{{\"Member\":\"{target.Member.FormKey}<TestGame.TestMajorRecord>\"}}";
394416
JsonConvert.DeserializeObject<FormLinkNullableClass>(toDeserialize, settings)!
395417
.Member
396418
.ShouldBe(target.Member);
@@ -447,7 +469,7 @@ public void FormKeyConverter_FormLinkNullable_Deserialize_FormKeyNull()
447469
{
448470
Member = new FormLinkNullable<ITestMajorRecordGetter>(FormKey.Null)
449471
};
450-
var toDeserialize = $"{{\"Member\":\"Null\"}}";
472+
var toDeserialize = $"{{\"Member\":\"Null<TestGame.TestMajorRecord>\"}}";
451473
JsonConvert.DeserializeObject<FormLinkNullableClass>(toDeserialize, settings)!
452474
.Member
453475
.ShouldBe(target.Member);
@@ -547,15 +569,14 @@ public void FormLinkInformationConverter_FormLink_Deserialize_Missing()
547569
[Fact]
548570
public void FormLinkInformationConverter_FormLink_Deserialize_Null()
549571
{
550-
Warmup.Init();
551572
var settings = new JsonSerializerSettings();
552573
settings.Converters.Add(new FormKeyJsonConverter());
553574
var target = new FormLinkInformationClass()
554575
{
555-
Interface = new FormLinkInformation(FormKey.Null, typeof(IMajorRecordGetter)),
556-
Direct = new FormLinkInformation(FormKey.Null, typeof(IMajorRecordGetter)),
576+
Interface = new FormLinkInformation(FormKey.Null, typeof(ITestMajorRecordGetter)),
577+
Direct = new FormLinkInformation(FormKey.Null, typeof(ITestMajorRecordGetter)),
557578
};
558-
var toDeserialize = $"{{\"Direct\":\"Null\",\"Interface\":\"Null\"}}";
579+
var toDeserialize = $"{{\"Direct\":\"Null<TestGame.TestMajorRecord>\",\"Interface\":\"Null<TestGame.TestMajorRecord>\"}}";
559580
JsonConvert.DeserializeObject<FormLinkInformationClass>(toDeserialize, settings)!
560581
.Direct
561582
.ShouldBe(target.Direct);
@@ -565,9 +586,24 @@ public void FormLinkInformationConverter_FormLink_Deserialize_Null()
565586
}
566587

567588
[Fact]
568-
public void FormLinkInformationConverter_FormLink_Deserialize()
589+
public void FormLinkInformationConverter_FormLink_Serialize_Deserialize_Generic_Interface()
590+
{
591+
var settings = new JsonSerializerSettings();
592+
settings.Converters.Add(new FormKeyJsonConverter());
593+
var formKey = FormKey.Factory("123456:Mod.esm");
594+
var target = new FormLinkInformationClass()
595+
{
596+
Interface = new FormLink<ITestMajorRecordGetter>(formKey),
597+
};
598+
var serialize = JsonConvert.SerializeObject(target, settings);
599+
JsonConvert.DeserializeObject<FormLinkInformationClass>(serialize, settings)!
600+
.Interface
601+
.ShouldBe(target.Interface);
602+
}
603+
604+
[Fact]
605+
public void FormLinkInformationConverter_FormLink_Deserialize_Null_BethesdaMajorRecord()
569606
{
570-
Warmup.Init();
571607
var settings = new JsonSerializerSettings();
572608
settings.Converters.Add(new FormKeyJsonConverter());
573609
var target = new FormLinkInformationClass()
@@ -585,9 +621,8 @@ public void FormLinkInformationConverter_FormLink_Deserialize()
585621
}
586622

587623
[Fact]
588-
public void FormLinkInformationConverter_FormLink_Serialize_Null()
624+
public void FormLinkInformationConverter_FormLink_Serialize_Null_BethesdaMajorRecord()
589625
{
590-
Warmup.Init();
591626
var settings = new JsonSerializerSettings();
592627
settings.Converters.Add(new FormKeyJsonConverter());
593628
var target = new FormLinkInformationClass()
@@ -600,10 +635,30 @@ public void FormLinkInformationConverter_FormLink_Serialize_Null()
600635
.ShouldBe(toDeserialize);
601636
}
602637

638+
[Fact]
639+
public void FormLinkInformationConverter_FormLink_Null_MajorRecord()
640+
{
641+
var settings = new JsonSerializerSettings();
642+
settings.Converters.Add(new FormKeyJsonConverter());
643+
var deserialized = new FormLinkInformationClass()
644+
{
645+
Interface = new FormLinkInformation(FormKey.Null, typeof(IMajorRecordGetter)),
646+
Direct = new FormLinkInformation(FormKey.Null, typeof(IMajorRecordGetter)),
647+
};
648+
var serialized = $"{{\"Interface\":\"Null<MajorRecord>\",\"Direct\":\"Null<MajorRecord>\"}}";
649+
JsonConvert.DeserializeObject<FormLinkInformationClass>(serialized, settings)!
650+
.Direct
651+
.ShouldBe(deserialized.Direct);
652+
JsonConvert.DeserializeObject<FormLinkInformationClass>(serialized, settings)!
653+
.Interface
654+
.ShouldBe(deserialized.Interface);
655+
JsonConvert.SerializeObject(deserialized, settings)
656+
.ShouldBe(serialized);
657+
}
658+
603659
[Fact]
604660
public void FormLinkInformationConverter_FormLink_Serialize()
605661
{
606-
Warmup.Init();
607662
var settings = new JsonSerializerSettings();
608663
settings.Converters.Add(new FormKeyJsonConverter());
609664
var target = new FormLinkInformationClass()
@@ -619,7 +674,6 @@ public void FormLinkInformationConverter_FormLink_Serialize()
619674
[Fact]
620675
public void FormLinkInformationConverter_FormLink_Deserialize_Empty()
621676
{
622-
Warmup.Init();
623677
var settings = new JsonSerializerSettings();
624678
settings.Converters.Add(new FormKeyJsonConverter());
625679
var target = new FormLinkInformationClass()

Mutagen.Bethesda.Core.UnitTests/Pex/BinaryExtensionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ private static void DoTest<T>(int streamCapacity, T expected, Action<BinaryWrite
7676
{
7777
using var ms = new MemoryStream(streamCapacity);
7878
using var bw = new BinaryWriter(ms);
79-
using var br = new BinaryReadStream(ms, isLittleEndian: false);
8079

8180
write(bw);
8281
ms.Position = 0;
8382

83+
using var br = new BinaryReadStream(ms, isLittleEndian: false);
8484
var actual = read(br);
8585
Assert.Equal(expected, actual);
8686
Assert.Equal(ms.Length, ms.Position);

Mutagen.Bethesda.Core.UnitTests/Placeholders/TestMajorRecord.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Mutagen.Bethesda.Plugins.Binary.Translations;
77
using Mutagen.Bethesda.Plugins.Cache;
88
using Mutagen.Bethesda.Plugins.Records;
9+
using Noggog;
910
using Noggog.StructuredStrings;
1011

1112
namespace Mutagen.Bethesda.UnitTests.Placeholders;
@@ -195,3 +196,31 @@ public IEnumerable<IAssetLinkGetter> EnumerateAssetLinks(AssetLinkQuery queryCat
195196
throw new NotImplementedException();
196197
}
197198
}
199+
internal class TestMajorRecord_Registration : ILoquiRegistration
200+
{
201+
public static TestMajorRecord_Registration Instance { get; } = new();
202+
203+
public string GetNthName(ushort index) => throw new NotImplementedException();
204+
public bool GetNthIsLoqui(ushort index) => throw new NotImplementedException();
205+
public bool GetNthIsEnumerable(ushort index) => throw new NotImplementedException();
206+
public bool GetNthIsSingleton(ushort index) => throw new NotImplementedException();
207+
public bool IsNthDerivative(ushort index) => throw new NotImplementedException();
208+
public Type GetNthType(ushort index) => throw new NotImplementedException();
209+
public ushort? GetNameIndex(StringCaseAgnostic name) => throw new NotImplementedException();
210+
public bool IsProtected(ushort index) => throw new NotImplementedException();
211+
public ProtocolKey ProtocolKey { get; } = new("TestGame");
212+
public ushort AdditionalFieldCount { get; }
213+
public ushort FieldCount { get; }
214+
public Type MaskType => null!;
215+
public Type ErrorMaskType => null!;
216+
public Type ClassType { get; } = typeof(TestMajorRecord);
217+
public Type GetterType { get; } = typeof(ITestMajorRecordGetter);
218+
public Type SetterType { get; } = typeof(ITestMajorRecord);
219+
public Type? InternalGetterType { get; }
220+
public Type? InternalSetterType { get; }
221+
public string FullName => "Mutagen.Bethesda.TestGame.TestMajorRecord";
222+
public string Name => "TestMajorRecord";
223+
public string Namespace => "Mutagen.Bethesda";
224+
public byte GenericCount { get; }
225+
public Type? GenericRegistrationType { get; }
226+
}

0 commit comments

Comments
 (0)