Design: Reference table file for any node member - BigBang1112/gbx-net GitHub Wiki
This is a design - plan for a potentially reliable solution better than the current one. It is expected to be majorly breaking or change major things.
- Planned for release if realized: 2.5+ or 3.0
There's a limitation with referencing nodes as files. As long as the node member doesn't have a ...File suffixed member, it cannot be used to reference another Gbx file. This can limit possiblities with node referencing if there's no ...File suffixed GbxRefTableFile, but it also bloats the class with additional ...File members, ruining the general overview of the class. It also introduces a lot of code duplication that was often generated but it also had to be pasted manually a couple of times.
The potential solution could've been a wrapper like External<T> where T : CMwNod, however, those introduce additional layer of access EntityModelEdition.Node.MeshCrystal.Node.Layers..., which is not wanted for code clarity and would be a large-scale breaking change.
Another possible solution could be discriminated unions, but those are more intended for distincting different objects under the same logical name, which makes them less practical to work with for simple purposes, and they don't reduce the access layer any further than the wrapper above. They are basically handy for Web APIs more than anything.
So, instead: the ...File could be moved to a member of CMwNod instead of being next to the node member.
Having the node file directly as part of the node allows the best from both worlds - easy member access, cleaner class structure, and any externally referencable node. But it's not for free:
- If there's an external node reference (doesn't matter if it exists on the drive), the type of the member is also instantiated (currently, if the file doesn't exist, the node is
null). - All currently abstract engine classes need to be made instantiable (remove
abstractkeywords) and supportnew T()ornew T(GbxRefTableFile), because the real type of the file cannot be determined without the file data itself. - If the external node exists on the drive, it is loaded automatically as usual, no breaking change there if you use just node members and not the file ones. If there's a parsing error on that node, it will throw an exception like it does since
2.3.0(2.2.2 and below return null on errors).
Up to here, as long as you have all Gbx references on a drive, this is not breaking, and users that use only these aspects shouldn't be affected.
If the external node doesn't exist on the drive, the file is stored in the new CMwNod.File or CMwNod.ReferenceFile member (TBD: find potential collisions, perhaps choose better name). The node member will no longer be null if the file is not available.
This creates a new problem where all of the node instance members are set to their default values, so it gives false assumption of everything working as expected, even though the default value policy will be unreliable and confusing in the end. To avoid that:
Throw a ReferenceFileNotFoundException styled form of exception when accessing a node member with an external file that's not present.
This can be annoying when using the library on Gbxs where not all of them are present in your folders (explorers, data exporters, partial data exports), places where you sometimes don't have all the files but you still try to apply a large scale data export, but those could be considered rare and an exception in all of the problems there. In places where it is not guaranteed by design to have all Gbx files present, there will need to be exception handling added (ways to reduce the syntax bloat to be explained below).
The ReferenceFileNotFoundException kind of exception will also provide the property File, referencing the original GbxRefTableFile, which you can use to visualize the missing file problem, and handle it less like an error if you need to (Explorer would need that very much).
Finally, the previous ...File member present in various places is removed.
The biggest problem is probably that the ReferenceFileNotFoundException can kill the entire following code while it would be preferable in many cases for it to just be nullable. Therefore, exception should be the default, and preference of null should be opt-in. It would look something like this:
interface INullableReferenceFiles<TNode> where TNode : CMwNod
{
TResult? AsNullable<TResult>(Func<TNode, TResult> memberFunc);
}
class CMwNod : INullableReferenceFiles<CMwNod>
{
public TResult? AsNullable<TResult>(Func<CMwNod, TResult> memberFunc)
{
try
{
return memberFunc(this);
}
catch (ReferenceFileNotFoundException)
{
return default;
}
}
}Additionally, you cannot replace a missing file in CMwNod.ReferenceFile directly due to the exception restricting the access, you will have to create a new node instance with the ReferenceFile set inside. This could be a flaw for picking the right class types, as all base classes could be theoretically passed, so a cleaner way to create "referenced nodes" should be considered.
Instead of passing the reference to the node member and the file member, only the node member reference is passed, always created, and in case of an external node, the file is stored inside the node instead of next to it.
If the node member is accessed with a property getter, the current member is swapped with the loaded one, and the file is referenced over. For async operation, some code might still need to be generated additionally.
If a node has CMwNod.File filled, it will always prioritize that when writing back. If it's null, it will be considered as an embedded node.
If the file is not found, the exception will be ignored and the file will be extracted from it.
Arrays and lists cannot offer the same experience as property getter. You cannot throw an exception when accessing specific elements. This may need to create a new type of list (something like NodList or NodArray). It doesn't need a complete revert to IList everywhere, as this is a concern only with nodes. AI generated example:
using System;
using System.Collections;
using System.Collections.Generic;
public class ThrowingList<T> : IList<T>
{
private readonly IList<T> _inner;
private readonly Func<int, T, bool> _shouldThrow;
public ThrowingList(IList<T> inner, Func<int, T, bool> shouldThrow)
{
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
_shouldThrow = shouldThrow ?? throw new ArgumentNullException(nameof(shouldThrow));
}
public T this[int index]
{
get
{
var value = _inner[index];
if (_shouldThrow(index, value))
throw new InvalidOperationException($"Access denied at index {index}");
return value;
}
set => _inner[index] = value;
}
public int Count => _inner.Count;
public bool IsReadOnly => _inner.IsReadOnly;
public IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < _inner.Count; i++)
{
var value = _inner[i];
if (_shouldThrow(i, value))
throw new InvalidOperationException($"Enumeration denied at index {i}");
yield return value;
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// Pass-through members
public void Add(T item) => _inner.Add(item);
public void Clear() => _inner.Clear();
public bool Contains(T item) => _inner.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
public int IndexOf(T item) => _inner.IndexOf(item);
public void Insert(int index, T item) => _inner.Insert(index, item);
public bool Remove(T item) => _inner.Remove(item);
public void RemoveAt(int index) => _inner.RemoveAt(index);
}The same should be obviously expected with other enumerables like Dictionary, but this type is luckily not recommended for data consistency (order especially) during de/serialization.
Every member referencing any CMwNod type will need to have specialized getter that will throw on missing reference table files. This may be wrapped into a single protected method that will handle it all, but it would be still better to generate it directly via v2 chunk generation, which should be realized earlier.
- Node member is one and only (no
...Fileextensions bloating in the way) - External node that is present but not found is considered an exception
- Any node member can be swapped with an external file
