• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zorbathut / dec / 6040990611

31 Aug 2023 06:16PM UTC coverage: 91.749% (+1.4%) from 90.365%
6040990611

push

github-ci

Ben Rog-Wilhelm
Add ModeDeleteIfExists tests.

3269 of 3563 relevant lines covered (91.75%)

115961.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.69
/src/ParserModular.cs
1
namespace Dec
2
{
3
    using System;
4
    using System.Collections.Generic;
5
    using System.IO;
6
    using System.Linq;
7
    using System.Reflection;
8
    using System.Runtime.CompilerServices;
9
    using System.Xml.Linq;
10

11
    /// <summary>
12
    /// Handles all parsing and initialization of dec structures.
13
    ///
14
    /// Intended for moddable games; use Parser for non-moddable or prototype games.
15
    /// </summary>
16
    public class ParserModular
17
    {
18
        public class Module : IParser
19
        {
20
            internal string name;
21
            internal readonly List<ReaderFileDec> readers = new List<ReaderFileDec>();
6,975✔
22

23
            /// <summary>
24
            /// Pass a directory in for recursive processing.
25
            /// </summary>
26
            /// <remarks>
27
            /// This function will ignore dot-prefixed directory names and files, which are common for development tools to create.
28
            /// </remarks>
29
            /// <param name="directory">The directory to look for files in.</param>
30
            public void AddDirectory(string directory)
31
            {
87✔
32
                foreach (var file in Directory.GetFiles(directory, "*.xml"))
579✔
33
                {
159✔
34
                    if (!System.IO.Path.GetFileName(file).StartsWith("."))
159✔
35
                    {
123✔
36
                        AddFile(Parser.FileType.Xml, file);
123✔
37
                    }
123✔
38
                }
159✔
39

40
                foreach (var subdir in Directory.GetDirectories(directory))
372✔
41
                {
60✔
42
                    if (!System.IO.Path.GetFileName(subdir).StartsWith("."))
60✔
43
                    {
24✔
44
                        AddDirectory(subdir);
24✔
45
                    }
24✔
46
                }
60✔
47
            }
87✔
48

49
            /// <summary>
50
            /// Pass a file in for processing.
51
            /// </summary>
52
            /// <param name="stringName">A human-readable identifier useful for debugging. Generally, the name of the file that the string was read from. Not required; will be derived from filename automatically.</param>
53
            public void AddFile(Parser.FileType fileType, string filename, string identifier = null)
54
            {
147✔
55
                if (identifier == null)
147✔
56
                {
147✔
57
                    // This is imperfect, but good enough. People can pass their own identifier in if they want something clever.
58
                    identifier = Path.GetFileName(filename);
147✔
59
                }
147✔
60

61
                using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read))
147✔
62
                {
147✔
63
                    AddStream(fileType, fs, identifier);
147✔
64
                }
147✔
65
            }
147✔
66

67
            /// <summary>
68
            /// Pass a stream in for processing.
69
            /// </summary>
70
            /// <param name="identifier">A human-readable identifier useful for debugging. Generally, the name of the file that the stream was built from. Not required; will be derived from filename automatically</param>
71
            public void AddStream(Parser.FileType fileType, Stream stream, string identifier = "(unnamed)")
72
            {
147✔
73
                using (var reader = new StreamReader(stream))
147✔
74
                {
147✔
75
                    AddTextReader(fileType, reader, identifier);
147✔
76
                }
147✔
77
            }
147✔
78

79
            /// <summary>
80
            /// Pass a string in for processing.
81
            /// </summary>
82
            /// <param name="identifier">A human-readable identifier useful for debugging. Generally, the name of the file that the string was built from. Not required, but helpful.</param>
83
            public void AddString(Parser.FileType fileType, string contents, string identifier = "(unnamed)")
84
            {
6,567✔
85
                // This is a really easy error to make; we might as well handle it.
86
                if (contents.EndsWith(".xml"))
6,567✔
87
                {
12✔
88
                    Dbg.Err($"It looks like you've passed the filename `{contents}` to AddString instead of the actual XML file. Either use AddFile() or pass the file contents in.");
12✔
89
                }
12✔
90

91
                using (var reader = new StringReader(contents))
6,567✔
92
                {
6,567✔
93
                    AddTextReader(fileType, reader, identifier);
6,567✔
94
                }
6,567✔
95
            }
6,567✔
96

97
            private void AddTextReader(Parser.FileType fileType, TextReader textReader, string identifier = "(unnamed)")
98
            {
6,714✔
99
                if (s_Status != Status.Accumulating)
6,714✔
100
                {
3✔
101
                    Dbg.Err($"Adding data while while the world is in {s_Status} state; should be {Status.Accumulating} state");
3✔
102
                }
3✔
103

104
                string bakedIdentifier = (name == "core") ? identifier : $"{name}:{identifier}";
6,714✔
105

106
                ReaderFileDec reader;
107

108
                if (fileType == Parser.FileType.Xml)
6,714✔
109
                {
6,714✔
110
                    reader = ReaderFileDecXml.Create(textReader, bakedIdentifier);
6,714✔
111
                }
6,714✔
112
                else
113
                {
×
114
                    Dbg.Err($"{bakedIdentifier}: Only XML files are supported at this time");
×
115
                    return;
×
116
                }
117

118
                if (reader != null)
6,714✔
119
                {
6,666✔
120
                    readers.Add(reader);
6,666✔
121
                }
6,666✔
122

123
                // otherwise, error has already been printed
124
            }
6,714✔
125
        }
126

127
        // Global status
128
        private enum Status
129
        {
130
            Uninitialized,
131
            Accumulating,
132
            Processing,
133
            Distributing,
134
            Finalizing,
135
            Finished,
136
        }
137
        private static Status s_Status = Status.Uninitialized;
138

139
        // Data stored from initialization parameters
140
        private List<Type> staticReferences = new List<Type>();
6,603✔
141

142
        // Modules
143
        internal List<Module> modules = new List<Module>();
6,603✔
144

145
        // A list of inheritance-based work that still has to be resolved
146
        private struct InheritanceJob
147
        {
148
            public Dec target;
149
            public ReaderNode node;
150
            public string parent;
151
        }
152

153
        // Used for static reference validation
154
        private static Action s_StaticReferenceHandler = null;
155

156
        /// <summary>
157
        /// Creates a Parser.
158
        /// </summary>
159
        public ParserModular()
6,603✔
160
        {
6,603✔
161
            if (s_Status != Status.Uninitialized)
6,603✔
162
            {
6✔
163
                Dbg.Err($"Parser created while the world is in {s_Status} state; should be {Status.Uninitialized} state");
6✔
164
            }
6✔
165
            s_Status = Status.Accumulating;
6,603✔
166

167
            bool unitTestMode = Config.TestParameters != null;
6,603✔
168

169
            {
6,603✔
170
                IEnumerable<Type> staticRefs;
171
                if (!unitTestMode)
6,603✔
172
                {
3✔
173
                    staticRefs = UtilReflection.GetAllUserTypes().Where(t => t.GetCustomAttribute<StaticReferencesAttribute>() != null);
4,079✔
174
                }
3✔
175
                else if (Config.TestParameters.explicitStaticRefs != null)
6,600✔
176
                {
213✔
177
                    staticRefs = Config.TestParameters.explicitStaticRefs;
213✔
178
                }
213✔
179
                else
180
                {
6,387✔
181
                    staticRefs = Enumerable.Empty<Type>();
6,387✔
182
                }
6,387✔
183

184
                foreach (var type in staticRefs)
20,241✔
185
                {
216✔
186
                    if (type.GetCustomAttribute<StaticReferencesAttribute>() == null)
216✔
187
                    {
18✔
188
                        Dbg.Err($"{type} is not tagged as StaticReferences");
18✔
189
                    }
18✔
190

191
                    if (!type.IsAbstract || !type.IsSealed)
216✔
192
                    {
18✔
193
                        Dbg.Err($"{type} is not static");
18✔
194
                    }
18✔
195

196
                    staticReferences.Add(type);
216✔
197
                }
216✔
198
            }
6,603✔
199

200
            Serialization.Initialize();
6,603✔
201
        }
6,603✔
202

203
        /// <summary>
204
        /// Creates and registers a new module with a given name.
205
        /// </summary>
206
        public Module CreateModule(string name)
207
        {
6,975✔
208
            var module = new Module();
6,975✔
209
            module.name = name;
6,975✔
210

211
            if (modules.Any(mod => mod.name == name))
7,392✔
212
            {
12✔
213
                Dbg.Err($"Created duplicate module {name}");
12✔
214
            }
12✔
215

216
            modules.Add(module);
6,975✔
217
            return module;
6,975✔
218
        }
6,975✔
219

220
        /// <summary>
221
        /// Finish all parsing.
222
        /// </summary>
223
        public void Finish()
224
        {
6,594✔
225
            System.GC.Collect();
6,594✔
226

227
            using (var _ = new CultureInfoScope(Config.CultureInfo))
6,594✔
228
            {
6,594✔
229
                if (s_Status != Status.Accumulating)
6,594✔
230
                {
3✔
231
                    Dbg.Err($"Finishing while the world is in {s_Status} state; should be {Status.Accumulating} state");
3✔
232
                }
3✔
233
                s_Status = Status.Processing;
6,594✔
234

235
                var readerContext = new ReaderContext(false);
6,594✔
236

237
                // Collate reader decs
238
                var registeredDecs = new Dictionary<(Type, string), List<ReaderFileDec.ReaderDec>>();
6,594✔
239
                foreach (var module in modules)
33,714✔
240
                {
6,966✔
241
                    var seenDecs = new Dictionary<(Type, string), ReaderNode>();
6,966✔
242
                    foreach (var reader in module.readers)
34,224✔
243
                    {
6,663✔
244
                        foreach (var readerDec in reader.ParseDecs())
73,737✔
245
                        {
26,874✔
246
                            var id = (readerDec.type.GetDecRootType(), readerDec.name);
26,874✔
247
                            
248
                            var collidingDec = seenDecs.TryGetValue(id);
26,874✔
249
                            if (collidingDec != null)
26,874✔
250
                            {
48✔
251
                                Dbg.Err($"{collidingDec.GetInputContext()} / {readerDec.node.GetInputContext()}: Dec [{id.Item1}:{id.Item2}] defined twice");
48✔
252

253
                                // If the already-parsed one is abstract, we throw it away and go with the non-abstract one, because it's arguably more likely to be the one the user wants.
254
                                if (!(registeredDecs[id].Select(dec => dec.abstrct).LastOrDefault(abstrct => abstrct.HasValue) ?? false))
144✔
255
                                {
24✔
256
                                    continue;
24✔
257
                                }
258

259
                                registeredDecs.Remove(id);
24✔
260
                            }
24✔
261
                            
262
                            seenDecs[id] = readerDec.node;
26,850✔
263

264
                            if (!registeredDecs.TryGetValue(id, out var list))
26,850✔
265
                            {
26,274✔
266
                                list = new List<ReaderFileDec.ReaderDec>();
26,274✔
267
                                registeredDecs[id] = list;
26,274✔
268
                            }
26,274✔
269
                            list.Add(readerDec);
26,850✔
270
                        }
26,850✔
271
                    }
6,663✔
272
                }
6,966✔
273

274
                // Compile reader decs into Order assemblies
275
                var registeredDecOrders = new Dictionary<(Type, string), List<ReaderFileDec.ReaderDec>>();
6,594✔
276
                foreach (var seenDec in registeredDecs)
72,282✔
277
                {
26,250✔
278
                    var orders = Serialization.CompileDecOrders(seenDec.Value);
26,250✔
279

280
                    // If we have no orders, this probably ended up deleted.
281
                    if (orders.Count > 0)
26,250✔
282
                    {
26,046✔
283
                        registeredDecOrders[seenDec.Key] = orders;
26,046✔
284
                    }
26,046✔
285
                }
26,250✔
286

287
                // Instantiate all decs
288
                var toParseDecOrders = new Dictionary<(Type, string), List<ReaderFileDec.ReaderDec>>();
6,594✔
289
                foreach (var (id, orders) in registeredDecOrders)
71,874✔
290
                {
26,046✔
291
                    // It's currently sort of unclear how we should be deriving the type.
292
                    // I'm choosing, for now, to go with "the most derived type in the list, assuming all types are in the same inheritance sequence".
293
                    // Thankfully this isn't too hard to do.
294
                    var typeDeterminor = orders[0];
26,046✔
295
                    bool abstrct = typeDeterminor.abstrct ?? false;
26,046✔
296

297
                    foreach (var order in orders.Skip(1))
78,504✔
298
                    {
192✔
299
                        // Since we're iterating over this anyway, yank the abstract updates out as go.
300
                        if (order.abstrct.HasValue)
192✔
301
                        {
48✔
302
                            abstrct = order.abstrct.Value;
48✔
303
                        }
48✔
304

305
                        if (order.type == typeDeterminor.type)
192✔
306
                        {
192✔
307
                            // fast case
308
                            continue;
192✔
309
                        }
310

311
                        if (order.type.IsSubclassOf(typeDeterminor.type))
×
312
                        {
×
313
                            typeDeterminor = order;
×
314
                            continue;
×
315
                        }
316

317
                        if (typeDeterminor.type.IsSubclassOf(order.type))
×
318
                        {
×
319
                            continue;
×
320
                        }
321

322
                        // oops, they're not subclasses of each other
323
                        Dbg.Err($"{typeDeterminor.inputContext} / {order.inputContext}: Modded dec with tree-identifier [{id.Item1}:{id.Item2}] has conflicting types without a simple subclass relationship ({typeDeterminor.type}/{order.type}); deferring to {order.type}");
×
324
                        typeDeterminor = order;
×
325
                    }
×
326

327
                    // We don't actually want an instance of this.
328
                    if (abstrct)
26,046✔
329
                    {
420✔
330
                        continue;
420✔
331
                    }
332

333
                    // Not an abstract dec instance, so create our instance
334
                    var decInstance = (Dec)typeDeterminor.type.CreateInstanceSafe("dec", typeDeterminor.node);
25,626✔
335

336
                    // Error reporting happens within CreateInstanceSafe; if we get null out, we just need to clean up elegantly
337
                    if (decInstance != null)
25,626✔
338
                    {
25,602✔
339
                        decInstance.DecName = id.Item2;
25,602✔
340

341
                        Database.Register(decInstance);
25,602✔
342

343
                        // create a new map that filters out abstract objects and failed instantiations
344
                        toParseDecOrders.Add(id, orders);
25,602✔
345
                    }
25,602✔
346
                }
25,626✔
347

348
                // It's time to actually pull references out of the database, so let's stop spitting out empty warnings.
349
                Database.SuppressEmptyWarning();
6,594✔
350

351
                foreach (var (id, orders) in toParseDecOrders)
70,986✔
352
                {
25,602✔
353
                    // Accumulate our orders
354
                    var completeOrders = orders;
25,602✔
355

356
                    var currentOrder = orders;
25,602✔
357
                    while (true)
26,166✔
358
                    {
26,166✔
359
                        // See if we have a parent
360
                        var decWithParent = currentOrder.Where(dec => dec.parent != null).LastOrDefault();
52,632✔
361
                        if (decWithParent.parent == null || decWithParent.parent == "")
26,166✔
362
                        {
25,566✔
363
                            break;
25,566✔
364
                        }
365

366
                        var parentId = (id.Item1, decWithParent.parent);
600✔
367
                        if (!registeredDecOrders.TryGetValue(parentId, out var parentDec))
600✔
368
                        {
36✔
369
                            Dbg.Err($"{decWithParent.node.GetInputContext()}: Dec [{decWithParent.type}:{id.Item2}] is attempting to use parent `[{parentId.Item1}:{parentId.parent}]`, but no such dec exists");
36✔
370
                            // guess we'll just try to build it from nothing
371
                            break;
36✔
372
                        }
373

374
                        completeOrders.InsertRange(0, parentDec);
564✔
375

376
                        currentOrder = parentDec;
564✔
377
                    }
564✔
378

379
                    var targetDec = Database.Get(id.Item1, id.Item2);
25,602✔
380
                    Serialization.ParseElement(completeOrders.Select(order => order.node).ToList(), targetDec.GetType(), targetDec, readerContext, new Recorder.Context(), isRootDec: true, ordersOverride: completeOrders.Select(order => (Serialization.ParseCommand.Patch, order.node)).ToList());
78,534✔
381
                }
25,602✔
382

383
                if (s_Status != Status.Processing)
6,594✔
384
                {
×
385
                    Dbg.Err($"Distributing while the world is in {s_Status} state; should be {Status.Processing} state");
×
386
                }
×
387
                s_Status = Status.Distributing;
6,594✔
388

389
                foreach (var stat in staticReferences)
20,214✔
390
                {
216✔
391
                    if (!StaticReferencesAttribute.StaticReferencesFilled.Contains(stat))
216✔
392
                    {
60✔
393
                        s_StaticReferenceHandler = () =>
60✔
394
                        {
96✔
395
                            s_StaticReferenceHandler = null;
96✔
396
                            StaticReferencesAttribute.StaticReferencesFilled.Add(stat);
96✔
397
                        };
96✔
398
                    }
60✔
399

400
                    bool touched = false;
216✔
401
                    foreach (var field in stat.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static))
1,476✔
402
                    {
414✔
403
                        var dec = Database.Get(field.FieldType, field.Name);
414✔
404
                        if (dec == null)
414✔
405
                        {
18✔
406
                            Dbg.Err($"Static reference class {stat} has member `{field.FieldType} {field.Name}` that does not correspond to any loaded Dec");
18✔
407
                            field.SetValue(null, null); // this is unnecessary, but it does kick the static constructor just in case we wouldn't do it otherwise
18✔
408
                        }
18✔
409
                        else if (!field.FieldType.IsAssignableFrom(dec.GetType()))
396✔
410
                        {
18✔
411
                            Dbg.Err($"Static reference class {stat} has member `{field.FieldType} {field.Name}` that is not compatible with actual {dec.GetType()} {dec}");
18✔
412
                            field.SetValue(null, null); // this is unnecessary, but it does kick the static constructor just in case we wouldn't do it otherwise
18✔
413
                        }
18✔
414
                        else
415
                        {
378✔
416
                            field.SetValue(null, dec);
378✔
417
                        }
378✔
418

419
                        touched = true;
414✔
420
                    }
414✔
421

422
                    if (s_StaticReferenceHandler != null)
216✔
423
                    {
24✔
424
                        if (touched)
24✔
425
                        {
21✔
426
                            // Otherwise we shouldn't even expect this to have been registered, but at least there's literally no fields in it so it doesn't matter
427
                            Dbg.Err($"Failed to properly register {stat}; you may be missing a call to Dec.StaticReferencesAttribute.Initialized() in its static constructor, or the class may already have been initialized elsewhere (this should have thrown an error)");
21✔
428
                        }
21✔
429
                        
430
                        s_StaticReferenceHandler = null;
24✔
431
                    }
24✔
432
                }
216✔
433

434
                if (s_Status != Status.Distributing)
6,594✔
435
                {
×
436
                    Dbg.Err($"Finalizing while the world is in {s_Status} state; should be {Status.Distributing} state");
×
437
                }
×
438
                s_Status = Status.Finalizing;
6,594✔
439

440
                foreach (var dec in Database.List)
70,986✔
441
                {
25,602✔
442
                    try
443
                    {
25,602✔
444
                        dec.ConfigErrors(err => Dbg.Err($"{dec}: {err}"));
25,632✔
445
                    }
25,566✔
446
                    catch (Exception e)
36✔
447
                    {
36✔
448
                        Dbg.Ex(e);
36✔
449
                    }
36✔
450
                }
25,602✔
451

452
                foreach (var dec in Database.List)
70,986✔
453
                {
25,602✔
454
                    try
455
                    {
25,602✔
456
                        dec.PostLoad(err => Dbg.Err($"{dec}: {err}"));
25,632✔
457
                    }
25,566✔
458
                    catch (Exception e)
36✔
459
                    {
36✔
460
                        Dbg.Ex(e);
36✔
461
                    }
36✔
462
                }
25,602✔
463

464
                if (s_Status != Status.Finalizing)
6,594✔
465
                {
×
466
                    Dbg.Err($"Completing while the world is in {s_Status} state; should be {Status.Finalizing} state");
×
467
                }
×
468
                s_Status = Status.Finished;
6,594✔
469
            }
6,594✔
470
        }
6,594✔
471

472
        internal static void Clear()
473
        {
12,825✔
474
            if (s_Status != Status.Finished && s_Status != Status.Uninitialized)
12,825✔
475
            {
9✔
476
                Dbg.Err($"Clearing while the world is in {s_Status} state; should be {Status.Uninitialized} state or {Status.Finished} state");
9✔
477
            }
9✔
478
            s_Status = Status.Uninitialized;
12,825✔
479
        }
12,825✔
480

481
        [MethodImpl(MethodImplOptions.NoInlining)]
482
        internal static void StaticReferencesInitialized()
483
        {
48✔
484
            if (s_StaticReferenceHandler != null)
48✔
485
            {
36✔
486
                s_StaticReferenceHandler();
36✔
487
                return;
36✔
488
            }
489

490
            Dbg.Err($"Initializing static reference class at an inappropriate time - this probably means you accessed a static reference class before it was ready");
12✔
491
        }
48✔
492
    }
493
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc