Added Arch Persitance and Relationships, styackable items, hot fixed Tags.containsAll. AutoList in Ordered!DList
This commit is contained in:
@@ -37,10 +37,10 @@
|
||||
<ItemGroup>
|
||||
<!-- Production dependencies go here! -->
|
||||
<PackageReference Include="Arch" Version="2.1.0" />
|
||||
<PackageReference Include="Arch-Events" Version="2.1.0" />
|
||||
<PackageReference Include="Arch.LowLevel" Version="1.1.5" />
|
||||
<PackageReference Include="Arch.System" Version="1.1.0" />
|
||||
<PackageReference Include="Chickensoft.GameTools" Version="3.1.18" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||
<PackageReference Include="NCalc.LambdaCompilation" Version="5.12.0" />
|
||||
<PackageReference Include="NCalcSync" Version="5.12.0" />
|
||||
<PackageReference Include="SjkScripts" Version="1.0.17" />
|
||||
@@ -59,8 +59,11 @@
|
||||
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.20.0" PrivateAssets="all" OutputItemType="analyzer" />
|
||||
<PackageReference Include="Chickensoft.UMLGenerator" Version="1.1.0" />
|
||||
<PackageReference Include="Chickensoft.Sync" Version="2.2.0" />
|
||||
<PackageReference Include="Utf8Json" Version="1.3.7" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="/mods" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RunTests)' == 'true'">
|
||||
<!-- Test dependencies go here! -->
|
||||
<!-- Dependencies added here will not be included in release builds. -->
|
||||
|
||||
8
GlobalSuppressions.cs
Normal file
8
GlobalSuppressions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("Style", "IDE0065:Misplaced using directive", Justification = "<Pending>")]
|
||||
49
export_presets.cfg
Normal file
49
export_presets.cfg
Normal file
@@ -0,0 +1,49 @@
|
||||
[preset.0]
|
||||
|
||||
name="Linux"
|
||||
platform="Linux"
|
||||
runnable=true
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter="/mods"
|
||||
export_path="../../Builds/ChickenGameTest.x86_64"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.0.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
shader_baker/enabled=false
|
||||
binary_format/architecture="x86_64"
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="#!/usr/bin/env bash
|
||||
export DISPLAY=:0
|
||||
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
||||
\"{temp_dir}/{exe_name}\" {cmd_args}"
|
||||
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||
pkill -x -f \"{temp_dir}/{exe_name} {cmd_args}\"
|
||||
rm -rf \"{temp_dir}\""
|
||||
dotnet/include_scripts_content=false
|
||||
dotnet/include_debug_symbols=true
|
||||
dotnet/embed_build_outputs=false
|
||||
0
mods/.gdignore
Normal file
0
mods/.gdignore
Normal file
50
src/Api/IFoodFactoryApi.cs
Normal file
50
src/Api/IFoodFactoryApi.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
namespace FoodFactory;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FoodFactory.Conveyors;
|
||||
using FoodFactory.Recipes;
|
||||
using FoodFactory.Voxel;
|
||||
|
||||
public interface IFoodFactoryApi
|
||||
{
|
||||
T? GetApi<T>() where T : class;
|
||||
IRecipes Recipes { get; }
|
||||
IBlueprintManger BlueprintManger { get; }
|
||||
ITickManger TickManger { get; }
|
||||
IItemRenderer ItemRenderer { get; }
|
||||
IVoxelGridRegistry GridRegistry { get; }
|
||||
}
|
||||
public interface ITickManger
|
||||
{
|
||||
delegate void Tick(TickArgs args);
|
||||
event Tick GameTick;
|
||||
}
|
||||
public record struct TickArgs(int Tick, double Delta);
|
||||
public class TickManger : ITickManger
|
||||
{
|
||||
public event ITickManger.Tick? GameTick;
|
||||
public void BroadCast(TickArgs args) => GameTick?.Invoke(args);
|
||||
}
|
||||
public class FoodFactoryApi : IFoodFactoryApi
|
||||
{
|
||||
public required IRecipes Recipes { get; set; }
|
||||
|
||||
public required IBlueprintManger BlueprintManger { get; set; }
|
||||
|
||||
public required ITickManger TickManger { get; set; }
|
||||
|
||||
public required IItemRenderer ItemRenderer { get; set; }
|
||||
|
||||
public required IVoxelGridRegistry GridRegistry { get; set; }
|
||||
|
||||
private readonly Dictionary<Type, object> _otherApi = [];
|
||||
public T? GetApi<T>() where T : class
|
||||
{
|
||||
if (_otherApi.TryGetValue(typeof(T), out var api))
|
||||
{
|
||||
return api as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
1
src/Api/IFoodFactoryApi.cs.uid
Normal file
1
src/Api/IFoodFactoryApi.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dbmq2c5nn8r1q
|
||||
497
src/Arch.Extended/Arch.Persistence/Binary.cs
Normal file
497
src/Arch.Extended/Arch.Persistence/Binary.cs
Normal file
@@ -0,0 +1,497 @@
|
||||
using Arch.Core;
|
||||
using Arch.Core.Extensions;
|
||||
using Arch.Core.Extensions.Dangerous;
|
||||
using Arch.Core.Utils;
|
||||
using Arch.LowLevel.Jagged;
|
||||
using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Utf8Json;
|
||||
|
||||
namespace Arch.Persistence;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SingleEntityFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter"/> to (de)serialize a single <see cref="Entity"/>to or from json.
|
||||
/// </summary>
|
||||
public partial class SingleEntityFormatter : IMessagePackFormatter<Entity>
|
||||
{
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, Entity value, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Write id
|
||||
writer.WriteInt32(value.Id);
|
||||
|
||||
#if !PURE_ECS
|
||||
|
||||
// Write world
|
||||
writer.WriteInt32(value.WorldId);
|
||||
#endif
|
||||
|
||||
// Write size
|
||||
var componentTypes = value.GetComponentTypes();
|
||||
writer.WriteInt32(componentTypes.Count);
|
||||
|
||||
// Write components
|
||||
foreach (ref var type in componentTypes.Components)
|
||||
{
|
||||
// Write type
|
||||
MessagePackSerializer.Serialize(ref writer, type, options);
|
||||
|
||||
// Write component
|
||||
var cmp = value.Get(type);
|
||||
MessagePackSerializer.Serialize(ref writer, cmp, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public Entity Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Read id
|
||||
var entityId = reader.ReadInt32();
|
||||
|
||||
#if !PURE_ECS
|
||||
|
||||
// Read world id
|
||||
var worldId = reader.ReadInt32();
|
||||
#endif
|
||||
|
||||
// Read size
|
||||
var size = reader.ReadInt32();
|
||||
var components = new object[size];
|
||||
|
||||
// Read components
|
||||
for (var index = 0; index < size; index++)
|
||||
{
|
||||
// Read type
|
||||
var type = MessagePackSerializer.Deserialize<ComponentType>(ref reader, options);
|
||||
var cmp = MessagePackSerializer.Deserialize(type, ref reader, options);
|
||||
components[index] = cmp!;
|
||||
}
|
||||
|
||||
// Create the entity
|
||||
var entity = EntityWorld.Create();
|
||||
EntityWorld.AddRange(entity, components.AsSpan());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="EntityFormatter"/> class
|
||||
/// is a formatter that (de)serializes <see cref="Entity"/> structs.
|
||||
/// </summary>
|
||||
public partial class EntityFormatter : IMessagePackFormatter<Entity>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, Entity value, MessagePackSerializerOptions options)
|
||||
{
|
||||
writer.WriteInt32(value.Id);
|
||||
writer.WriteInt32(value.Version);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public Entity Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Read id
|
||||
var id = reader.ReadInt32();
|
||||
var version = reader.ReadInt32();
|
||||
return DangerousEntityExtensions.CreateEntityStruct(id, WorldId, version);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArrayFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{Array}"/> to (de)serialize <see cref="Array"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ArrayFormatter : IMessagePackFormatter<Array>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, Array value, MessagePackSerializerOptions options)
|
||||
{
|
||||
var type = value.GetType().GetElementType();
|
||||
|
||||
// Write type and size
|
||||
MessagePackSerializer.Serialize(ref writer, type, options);
|
||||
writer.WriteUInt32((uint)value.Length);
|
||||
|
||||
// Write array
|
||||
for (var index = 0; index < value.Length; index++)
|
||||
{
|
||||
var obj = value.GetValue(index);
|
||||
MessagePackSerializer.Serialize(ref writer, obj, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public Array Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Write type and size
|
||||
var type = MessagePackSerializer.Deserialize<Type>(ref reader, options);
|
||||
var size = reader.ReadUInt32();
|
||||
|
||||
// Create array
|
||||
var array = Array.CreateInstance(type, size);
|
||||
|
||||
// Read array
|
||||
for (var index = 0; index < size; index++)
|
||||
{
|
||||
var obj = MessagePackSerializer.Deserialize(type, ref reader, options);
|
||||
array.SetValue(obj, index);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="JaggedArrayFormatter{T}"/> class
|
||||
/// (de)serializes a <see cref="JaggedArray{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type stored in the <see cref="JaggedArray{T}"/>.</typeparam>
|
||||
public partial class JaggedArrayFormatter<T> : IMessagePackFormatter<JaggedArray<T>>
|
||||
{
|
||||
private const int CpuL1CacheSize = 16_384;
|
||||
private readonly T _filler;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="filler">Filler.</param>
|
||||
public JaggedArrayFormatter(T filler)
|
||||
{
|
||||
_filler = filler;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, JaggedArray<T> value, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Write length/capacity and items
|
||||
writer.WriteInt32(value.Capacity);
|
||||
for (var index = 0; index < value.Capacity; index++)
|
||||
{
|
||||
var item = value[index];
|
||||
MessagePackSerializer.Serialize(ref writer, item, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public JaggedArray<T> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
var capacity = reader.ReadInt32();
|
||||
var jaggedArray = new JaggedArray<T>(CpuL1CacheSize / Unsafe.SizeOf<T>(), _filler, capacity);
|
||||
|
||||
for (var index = 0; index < capacity; index++)
|
||||
{
|
||||
var item = MessagePackSerializer.Deserialize<T>(ref reader, options);
|
||||
jaggedArray.Add(index, item);
|
||||
}
|
||||
|
||||
return jaggedArray;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ComponentTypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{ComponentType}"/> to (de)serialize <see cref="ComponentType"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ComponentTypeFormatter : IMessagePackFormatter<ComponentType>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, ComponentType value, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Write id
|
||||
writer.WriteUInt32((uint)value.Id);
|
||||
|
||||
// Write bytesize
|
||||
writer.WriteUInt32((uint)value.ByteSize);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public ComponentType Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
var id = reader.ReadUInt32();
|
||||
var bytesize = reader.ReadUInt32();
|
||||
|
||||
return new ComponentType((int)id, (int)bytesize);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ComponentTypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{ComponentType}"/> to (de)serialize <see cref="Signature"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class SignatureFormatter : IMessagePackFormatter<Signature>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, Signature value, MessagePackSerializerOptions options)
|
||||
{
|
||||
var componentTypeFormatter = options.Resolver.GetFormatter<ComponentType>() as ComponentTypeFormatter;
|
||||
|
||||
// Write count and types
|
||||
writer.WriteUInt32((uint)value.Count);
|
||||
foreach (var type in value.Components)
|
||||
{
|
||||
componentTypeFormatter!.Serialize(ref writer, type, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public Signature Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
var componentTypeFormatter = options.Resolver.GetFormatter<ComponentType>() as ComponentTypeFormatter;
|
||||
|
||||
// Read count
|
||||
var count = reader.ReadUInt32();
|
||||
|
||||
// Read types
|
||||
var componentTypes = new ComponentType[count];
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
var componentType = componentTypeFormatter!.Deserialize(ref reader, options);
|
||||
componentTypes[index] = componentType;
|
||||
}
|
||||
return new Signature(componentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ComponentTypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{ComponentType}"/> to (de)serialize <see cref="ComponentType"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class EntitySlotFormatter : IMessagePackFormatter<EntityData>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, EntityData value, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Write chunk index
|
||||
writer.WriteUInt32((uint)value.Slot.ChunkIndex);
|
||||
|
||||
// Write entity index
|
||||
writer.WriteUInt32((uint)value.Slot.Index);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public EntityData Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
|
||||
// Read chunk index and entity index
|
||||
var chunkIndex = reader.ReadUInt32();
|
||||
var entityIndex = reader.ReadUInt32();
|
||||
|
||||
return new EntityData(null!, new Slot((int)entityIndex, (int)chunkIndex), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="WorldFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{World}"/> to (de)serialize <see cref="World"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class WorldFormatter : IMessagePackFormatter<World>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, World value, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Write important meta data
|
||||
writer.WriteUInt32((uint)value.BaseChunkSize);
|
||||
writer.WriteUInt32((uint)value.BaseChunkEntityCount);
|
||||
|
||||
// Write slots
|
||||
MessagePackSerializer.Serialize(ref writer, value.GetEntityDataArray(), options);
|
||||
|
||||
//Write recycled entity ids
|
||||
var recycledEntityIDs = value.GetRecycledEntityIds();
|
||||
MessagePackSerializer.Serialize(ref writer, recycledEntityIDs, options);
|
||||
|
||||
// Write archetypes
|
||||
writer.WriteUInt32((uint)value.Archetypes.Count);
|
||||
foreach (var archetype in value)
|
||||
{
|
||||
MessagePackSerializer.Serialize(ref writer, archetype, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public World Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Read important metadata
|
||||
var baseChunkSize = reader.ReadUInt32();
|
||||
var baseChunkEntityCount = reader.ReadUInt32();
|
||||
|
||||
// Create world and setup formatter
|
||||
var world = World.Create(chunkSizeInBytes: (int)baseChunkSize, minimumAmountOfEntitiesPerChunk: (int)baseChunkEntityCount);
|
||||
var archetypeFormatter = options.Resolver.GetFormatter<Archetype>() as ArchetypeFormatter;
|
||||
var entityFormatter = options.Resolver.GetFormatter<Entity>() as EntityFormatter;
|
||||
entityFormatter!.WorldId = world.Id;
|
||||
archetypeFormatter!.World = world;
|
||||
|
||||
// Read slots
|
||||
var slots = MessagePackSerializer.Deserialize<JaggedArray<EntityData>>(ref reader, options);
|
||||
|
||||
//Read recycled entity ids
|
||||
var recycledEntityIDs = MessagePackSerializer.Deserialize<List<(int, int)>>(ref reader, options);
|
||||
|
||||
// Forward values to the world
|
||||
world.SetRecycledEntityIds(recycledEntityIDs);
|
||||
world.SetEntityDataArray(slots);
|
||||
world.EnsureCapacity(slots.Capacity);
|
||||
|
||||
// Read archetypes
|
||||
var size = reader.ReadInt32();
|
||||
List<Archetype> archetypes = new();
|
||||
|
||||
for (var index = 0; index < size; index++)
|
||||
{
|
||||
var archetype = archetypeFormatter.Deserialize(ref reader, options);
|
||||
archetypes.Add(archetype);
|
||||
}
|
||||
|
||||
// Set archetypes
|
||||
world.SetArchetypes(archetypes);
|
||||
return world;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArchetypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{Archetype}"/> to (de)serialize <see cref="Archetype"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ArchetypeFormatter : IMessagePackFormatter<Archetype>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, Archetype value, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Setup formatters
|
||||
var types = value.Signature;
|
||||
var chunks = value.Chunks;
|
||||
var chunkFormatter = options.Resolver.GetFormatter<Chunk>() as ChunkFormatter;
|
||||
chunkFormatter!.Signature = types;
|
||||
|
||||
// Write type array
|
||||
MessagePackSerializer.Serialize(ref writer, types, options);
|
||||
|
||||
// Write lookup array
|
||||
MessagePackSerializer.Serialize(ref writer, value.GetLookupArray(), options);
|
||||
|
||||
// Write chunk size
|
||||
writer.WriteUInt32((uint)value.ChunkCount);
|
||||
|
||||
// Write chunks
|
||||
for (var index = 0; index < value.ChunkCount; index++)
|
||||
{
|
||||
ref var chunk = ref chunks[index];
|
||||
chunkFormatter.Serialize(ref writer, chunk, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public Archetype Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
|
||||
var chunkFormatter = options.Resolver.GetFormatter<Chunk>() as ChunkFormatter;
|
||||
|
||||
// Types
|
||||
var types = MessagePackSerializer.Deserialize<Signature>(ref reader, options);
|
||||
|
||||
// Archetype lookup array
|
||||
var lookupArray = MessagePackSerializer.Deserialize<int[]>(ref reader, options);
|
||||
|
||||
// Archetype chunk size and list
|
||||
var chunkSize = reader.ReadUInt32();
|
||||
|
||||
// Create archetype
|
||||
var chunks = new List<Chunk>((int)chunkSize);
|
||||
var archetype = DangerousArchetypeExtensions.CreateArchetype(World.BaseChunkSize, World.BaseChunkEntityCount, types);
|
||||
archetype.Chunks.Clear(true);
|
||||
archetype.SetCount((int)chunkSize - 1);
|
||||
|
||||
// Pass types and lookup array to the chunk formatter for saving performance and memory
|
||||
chunkFormatter!.World = World;
|
||||
chunkFormatter.Archetype = archetype;
|
||||
chunkFormatter.Signature = types;
|
||||
chunkFormatter.LookupArray = lookupArray;
|
||||
|
||||
// Deserialise each chunk and put it into the archetype.
|
||||
var entities = 0;
|
||||
for (var index = 0; index < chunkSize; index++)
|
||||
{
|
||||
var chunk = chunkFormatter.Deserialize(ref reader, options);
|
||||
chunks.Add(chunk);
|
||||
entities += chunk.Count;
|
||||
}
|
||||
|
||||
archetype.SetChunks(chunks);
|
||||
archetype.SetEntities(entities);
|
||||
return archetype;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChunkFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{Chunk}"/> to (de)serialize <see cref="Chunk"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ChunkFormatter : IMessagePackFormatter<Chunk>
|
||||
{
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref MessagePackWriter writer, Chunk value, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Write size
|
||||
writer.WriteUInt32((uint)value.Count);
|
||||
|
||||
// Write capacity
|
||||
writer.WriteUInt32((uint)value.Capacity);
|
||||
|
||||
// Write entitys
|
||||
MessagePackSerializer.Serialize(ref writer, value.Entities, options);
|
||||
|
||||
// Persist arrays as an array...
|
||||
foreach (var type in Signature.Components)
|
||||
{
|
||||
// Write array itself
|
||||
var array = value.GetArray(type);
|
||||
MessagePackSerializer.Serialize(ref writer, array, options);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IMessagePackFormatter{T}.Deserialize"/>
|
||||
public Chunk Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
|
||||
{
|
||||
// Read chunk size
|
||||
var size = reader.ReadUInt32();
|
||||
|
||||
// Read chunk size
|
||||
var capacity = reader.ReadUInt32();
|
||||
|
||||
// Read entities
|
||||
var entities = MessagePackSerializer.Deserialize<Entity[]>(ref reader, options);
|
||||
|
||||
// Create chunk
|
||||
var chunk = DangerousChunkExtensions.CreateChunk((int)capacity, LookupArray, Signature);
|
||||
entities.CopyTo(chunk.Entities, 0);
|
||||
chunk.SetSize((int)size);
|
||||
|
||||
// Updating World.EntityInfoStorage to their new archetype
|
||||
for (var index = 0; index < size; index++)
|
||||
{
|
||||
ref var entity = ref chunk.Entity(index);
|
||||
entity = DangerousEntityExtensions.CreateEntityStruct(entity.Id, World.Id, entity.Version);
|
||||
World.SetArchetype(entity, Archetype);
|
||||
}
|
||||
|
||||
// Persist arrays as an array...
|
||||
foreach (var type in Signature.Components)
|
||||
{
|
||||
// Read array of the type
|
||||
var array = MessagePackSerializer.Deserialize<Array>(ref reader, options);
|
||||
var chunkArray = chunk.GetArray(array.GetType().GetElementType()!);
|
||||
Array.Copy(array, chunkArray, (int)size);
|
||||
}
|
||||
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
822
src/Arch.Extended/Arch.Persistence/Json.cs
Normal file
822
src/Arch.Extended/Arch.Persistence/Json.cs
Normal file
@@ -0,0 +1,822 @@
|
||||
namespace Arch.Persistence;
|
||||
|
||||
using global::System;
|
||||
using global::System.Collections.Generic;
|
||||
using Arch.Core;
|
||||
using Arch.Core.Extensions;
|
||||
using Arch.Core.Extensions.Dangerous;
|
||||
using Arch.LowLevel.Jagged;
|
||||
using CommunityToolkit.HighPerformance;
|
||||
using global::System.Runtime.CompilerServices;
|
||||
using Utf8Json;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SingleEntityFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{Entity}"/> to (de)serialize a single <see cref="Entity"/>to or from json.
|
||||
/// </summary>
|
||||
public partial class SingleEntityFormatter : IJsonFormatter<Entity>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="EntityWorld"/> the entity belongs to.
|
||||
/// </summary>
|
||||
internal World EntityWorld { get; set; } = null!;
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, Entity value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// Write id
|
||||
writer.WritePropertyName("id");
|
||||
writer.WriteInt32(value.Id);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
#if !PURE_ECS
|
||||
|
||||
// Write world
|
||||
writer.WritePropertyName("worldId");
|
||||
writer.WriteInt32(value.WorldId);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
#endif
|
||||
|
||||
// Write size
|
||||
var componentTypes = value.GetComponentTypes();
|
||||
writer.WritePropertyName("size");
|
||||
writer.WriteInt32(componentTypes.Count);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write components
|
||||
writer.WritePropertyName("components");
|
||||
writer.WriteBeginArray();
|
||||
foreach (ref var type in componentTypes.Components)
|
||||
{
|
||||
// Write type
|
||||
writer.WriteBeginObject();
|
||||
writer.WritePropertyName("type");
|
||||
JsonSerializer.Serialize(ref writer, type);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write component
|
||||
writer.WritePropertyName("component");
|
||||
var cmp = value.Get(type);
|
||||
JsonSerializer.NonGeneric.Serialize(ref writer, cmp, formatterResolver);
|
||||
writer.WriteEndObject();
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
writer.AdvanceOffset(-1);
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public Entity Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
// Read id
|
||||
reader.ReadPropertyName();
|
||||
var entityId = reader.ReadInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
#if !PURE_ECS
|
||||
|
||||
// Read world id
|
||||
reader.ReadPropertyName();
|
||||
var worldId = reader.ReadInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
#endif
|
||||
|
||||
// Read size
|
||||
reader.ReadPropertyName();
|
||||
var size = reader.ReadInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
var components = new object[size];
|
||||
var count = 0;
|
||||
|
||||
// Read components
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
while (!reader.ReadIsEndArrayWithSkipValueSeparator(ref count))
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
// Read type
|
||||
reader.ReadPropertyName();
|
||||
var type = JsonSerializer.Deserialize<ComponentType>(ref reader);
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
reader.ReadPropertyName();
|
||||
var cmp = JsonSerializer.NonGeneric.Deserialize(type.Type, ref reader, formatterResolver);
|
||||
components[count - 1] = cmp;
|
||||
reader.ReadIsEndObject();
|
||||
}
|
||||
|
||||
// Creat the entity
|
||||
var entity = EntityWorld.Create();
|
||||
EntityWorld.AddRange(entity, components.AsSpan());
|
||||
|
||||
reader.ReadIsEndObject();
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class EntityFormatter : IJsonFormatter<Entity>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="World.Id"/> all deserialized <see cref="Entity"/>s will belong to.
|
||||
/// <remarks>Due to the nature of deserialisation and changing world landscape we need to assign new WorldIds to the deserialized entities.</remarks>
|
||||
/// </summary>
|
||||
internal int WorldId { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, Entity value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
writer.WriteInt32(value.Id);
|
||||
writer.WriteValueSeparator();
|
||||
writer.WriteInt32(value.Version);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public Entity Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
// Read id
|
||||
var id = reader.ReadInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
var version = reader.ReadInt32();
|
||||
return DangerousEntityExtensions.CreateEntityStruct(id, WorldId, version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArrayFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{Array}"/> to (de)serialize <see cref="Array"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ArrayFormatter : IJsonFormatter<Array>
|
||||
{
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, Array value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
var type = value.GetType().GetElementType();
|
||||
|
||||
// Write type and size
|
||||
writer.WriteBeginObject();
|
||||
writer.WritePropertyName("type");
|
||||
JsonSerializer.Serialize(ref writer, type, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
writer.WritePropertyName("length");
|
||||
writer.WriteUInt32((uint)value.Length);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write array
|
||||
writer.WritePropertyName("items");
|
||||
writer.WriteBeginArray();
|
||||
for (var index = 0; index < value.Length; index++)
|
||||
{
|
||||
var obj = value.GetValue(index);
|
||||
JsonSerializer.NonGeneric.Serialize(ref writer, obj, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
writer.AdvanceOffset(-1);
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public Array Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
// Write type and size
|
||||
reader.ReadIsBeginObject();
|
||||
reader.ReadPropertyName();
|
||||
var type = JsonSerializer.Deserialize<Type>(ref reader, formatterResolver);
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
reader.ReadPropertyName();
|
||||
var size = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Create array
|
||||
var array = Array.CreateInstance(type, size);
|
||||
|
||||
// Read array
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
for (var index = 0; index < size; index++)
|
||||
{
|
||||
var obj = JsonSerializer.NonGeneric.Deserialize(type, ref reader, formatterResolver);
|
||||
array.SetValue(obj, index);
|
||||
reader.ReadIsValueSeparator();
|
||||
}
|
||||
reader.ReadIsEndArray();
|
||||
reader.ReadIsEndObject();
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="JaggedArrayFormatter{T}"/> class
|
||||
/// (de)serializes a <see cref="JaggedArray{T}"/>.
|
||||
/// </summary>
|
||||
public partial class JaggedArrayFormatter<T> : IJsonFormatter<JaggedArray<T>>
|
||||
{
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, JaggedArray<T> value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// Write length/capacity and items
|
||||
writer.WritePropertyName("capacity");
|
||||
writer.WriteInt32(value.Capacity);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write items
|
||||
writer.WritePropertyName("items");
|
||||
writer.WriteBeginArray();
|
||||
|
||||
for (var index = 0; index < value.Capacity; index++)
|
||||
{
|
||||
var item = value[index];
|
||||
JsonSerializer.Serialize(ref writer, item, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
|
||||
// Cut last value seperator
|
||||
if (value.Capacity > 0)
|
||||
{
|
||||
writer.AdvanceOffset(-1);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public JaggedArray<T> Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
// Read capacity;
|
||||
reader.ReadPropertyName();
|
||||
var capacity = reader.ReadInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Read items
|
||||
var jaggedArray = new JaggedArray<T>(CpuL1CacheSize / Unsafe.SizeOf<T>(), _filler, capacity);
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
for (var index = 0; index < capacity; index++)
|
||||
{
|
||||
var item = JsonSerializer.Deserialize<T>(ref reader, formatterResolver);
|
||||
jaggedArray.Add(index, item);
|
||||
reader.ReadIsValueSeparator();
|
||||
}
|
||||
reader.ReadIsEndArray();
|
||||
reader.ReadIsEndObject();
|
||||
|
||||
return jaggedArray;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ComponentTypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{ComponentType}"/> to (de)serialize <see cref="ComponentType"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ComponentTypeFormatter : IJsonFormatter<ComponentType>
|
||||
{
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, ComponentType value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// Write id
|
||||
writer.WritePropertyName("id");
|
||||
writer.WriteUInt32((uint)value.Id);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write bytesize
|
||||
writer.WritePropertyName("byteSize");
|
||||
writer.WriteUInt32((uint)value.ByteSize);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public ComponentType Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
reader.ReadPropertyName();
|
||||
var id = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
reader.ReadPropertyName();
|
||||
var bytesize = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
reader.ReadIsEndObject();
|
||||
|
||||
return new ComponentType((int)id, (int)bytesize);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ComponentTypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{ComponentType}"/> to (de)serialize <see cref="Signature"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class SignatureFormatter : IJsonFormatter<Signature>
|
||||
{
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, Signature value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
var componentTypeFormatter = formatterResolver.GetFormatter<ComponentType>() as ComponentTypeFormatter;
|
||||
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// write Count
|
||||
writer.WritePropertyName("count");
|
||||
writer.WriteUInt32((uint)value.Count);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write components
|
||||
writer.WritePropertyName("components");
|
||||
writer.WriteBeginArray();
|
||||
|
||||
foreach (var type in value.Components)
|
||||
{
|
||||
componentTypeFormatter!.Serialize(ref writer, type, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
|
||||
// Cut last value seperator
|
||||
if (value.Count > 0)
|
||||
{
|
||||
writer.AdvanceOffset(-1);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public Signature Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
var componentTypeFormatter = formatterResolver.GetFormatter<ComponentType>() as ComponentTypeFormatter;
|
||||
|
||||
reader.ReadIsBeginObject();
|
||||
reader.ReadPropertyName();
|
||||
|
||||
// Read count
|
||||
var count = (int)reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Read types
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
|
||||
var componentTypes = new ComponentType[count];
|
||||
count = 0;
|
||||
while (!reader.ReadIsEndArrayWithSkipValueSeparator(ref count))
|
||||
{
|
||||
var archetype = componentTypeFormatter!.Deserialize(ref reader, formatterResolver);
|
||||
componentTypes[count - 1] = (archetype);
|
||||
}
|
||||
|
||||
// Set archetypes
|
||||
reader.ReadIsEndObject();
|
||||
return new Signature(componentTypes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ComponentTypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{ComponentType}"/> to (de)serialize <see cref="ComponentType"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class EntitySlotFormatter : IJsonFormatter<EntityData>
|
||||
{
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, EntityData value, IJsonFormatterResolver options)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// Write chunk index
|
||||
writer.WritePropertyName("chunkIndex");
|
||||
writer.WriteUInt32((uint)value.Slot.ChunkIndex);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write entity index
|
||||
writer.WritePropertyName("index");
|
||||
writer.WriteUInt32((uint)value.Slot.Index);
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public EntityData Deserialize(ref JsonReader reader, IJsonFormatterResolver options)
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
// Read chunk index
|
||||
reader.ReadPropertyName();
|
||||
var chunkIndex = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Read entity index
|
||||
reader.ReadPropertyName();
|
||||
var entityIndex = reader.ReadUInt32();
|
||||
|
||||
reader.ReadIsEndObject();
|
||||
return new EntityData(null!, new Slot((int)entityIndex, (int)chunkIndex), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="WorldFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{World}"/> to (de)serialize <see cref="World"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class WorldFormatter : IJsonFormatter<World>
|
||||
{
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, World value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
//var archetypeFormatter = formatterResolver.GetFormatter<Archetype>();
|
||||
//var versionsFormatter = formatterResolver.GetFormatter<int[][]>();
|
||||
//var slotFormatter = formatterResolver.GetFormatter<(int,int)[][]>();
|
||||
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// Write meta data
|
||||
writer.WritePropertyName("baseChunkSize");
|
||||
writer.WriteUInt32((uint)value.BaseChunkSize);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
writer.WritePropertyName("baseChunkEntityCount");
|
||||
writer.WriteUInt32((uint)value.BaseChunkEntityCount);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write slots
|
||||
writer.WritePropertyName("slots");
|
||||
JsonSerializer.Serialize(ref writer, value.GetEntityDataArray(), formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
//Write recycled entity ids
|
||||
writer.WritePropertyName("recycledEntityIDs");
|
||||
writer.WriteBeginArray();
|
||||
var recycledEntityIDs = value.GetRecycledEntityIds();
|
||||
foreach (var recycledId in recycledEntityIDs)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
writer.WritePropertyName("id");
|
||||
writer.WriteInt32(recycledId.Item1);
|
||||
writer.WriteValueSeparator();
|
||||
writer.WritePropertyName("version");
|
||||
writer.WriteInt32(recycledId.Item2);
|
||||
writer.WriteEndObject();
|
||||
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
// Cut last value seperator
|
||||
if (recycledEntityIDs.Count > 0)
|
||||
{
|
||||
writer.AdvanceOffset(-1);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
//Write archetypes
|
||||
writer.WritePropertyName("archetypes");
|
||||
writer.WriteBeginArray();
|
||||
foreach (var archetype in value)
|
||||
{
|
||||
JsonSerializer.Serialize(ref writer, archetype, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
|
||||
// Cut last value seperator
|
||||
if (value.Archetypes.Count > 0)
|
||||
{
|
||||
writer.AdvanceOffset(-1);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public World Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
// Create world and setup formatter
|
||||
var archetypeFormatter = formatterResolver.GetFormatter<Archetype>() as ArchetypeFormatter;
|
||||
var entityFormatter = formatterResolver.GetFormatter<Entity>() as EntityFormatter;
|
||||
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
// Read meta data
|
||||
reader.ReadPropertyName();
|
||||
var baseChunkSize = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
reader.ReadPropertyName();
|
||||
var baseChunkEntityCount = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Construct world
|
||||
var world = World.Create(chunkSizeInBytes: (int)baseChunkSize, minimumAmountOfEntitiesPerChunk: (int)baseChunkEntityCount);
|
||||
entityFormatter!.WorldId = world.Id;
|
||||
archetypeFormatter!.World = world;
|
||||
|
||||
// Read slots
|
||||
reader.ReadPropertyName();
|
||||
var slots = JsonSerializer.Deserialize<JaggedArray<EntityData>>(ref reader, formatterResolver);
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Read recycled ids
|
||||
var count = 0;
|
||||
List<(int, int)> recycledIds = new();
|
||||
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
|
||||
while (!reader.ReadIsEndArrayWithSkipValueSeparator(ref count))
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
reader.ReadPropertyName();
|
||||
var id = reader.ReadInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
reader.ReadPropertyName();
|
||||
var value = reader.ReadInt32();
|
||||
reader.ReadIsEndObject();
|
||||
|
||||
(int, int) recycledId = new(id, value);
|
||||
|
||||
recycledIds.Add(recycledId);
|
||||
}
|
||||
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Forward values to the world
|
||||
world.SetRecycledEntityIds(recycledIds);
|
||||
world.SetEntityDataArray(slots);
|
||||
world.EnsureCapacity(slots.Capacity);
|
||||
|
||||
// Read archetypes
|
||||
count = 0;
|
||||
List<Archetype> archetypes = new();
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
while (!reader.ReadIsEndArrayWithSkipValueSeparator(ref count))
|
||||
{
|
||||
var archetype = archetypeFormatter.Deserialize(ref reader, formatterResolver);
|
||||
archetypes.Add(archetype);
|
||||
}
|
||||
|
||||
// Set archetypes
|
||||
world.SetArchetypes(archetypes);
|
||||
reader.ReadIsEndObject();
|
||||
return world;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArchetypeFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{Archetype}"/> to (de)serialize <see cref="Archetype"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ArchetypeFormatter : IJsonFormatter<Archetype>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="World"/> which is being used by this formatter during serialisation/deserialisation.
|
||||
/// </summary>
|
||||
internal World World { get; set; } = null!;
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, Archetype value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
// Setup formatters
|
||||
var types = value.Signature;
|
||||
var chunks = value.Chunks;
|
||||
var chunkFormatter = formatterResolver.GetFormatter<Chunk>() as ChunkFormatter;
|
||||
chunkFormatter!.Signature = types;
|
||||
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// Write type array
|
||||
writer.WritePropertyName("types");
|
||||
JsonSerializer.Serialize(ref writer, types, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write lookup array
|
||||
writer.WritePropertyName("lookup");
|
||||
JsonSerializer.Serialize(ref writer, value.GetLookupArray(), formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write chunk size
|
||||
writer.WritePropertyName("chunkCount");
|
||||
writer.WriteUInt32((uint)value.ChunkCount);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write chunks
|
||||
writer.WritePropertyName("chunks");
|
||||
writer.WriteBeginArray();
|
||||
for (var index = 0; index < value.ChunkCount; index++)
|
||||
{
|
||||
ref var chunk = ref chunks[index];
|
||||
chunkFormatter.Serialize(ref writer, chunk, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
|
||||
// Trim last value separator
|
||||
if (value.ChunkCount > 0)
|
||||
{
|
||||
writer.AdvanceOffset(-1);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public Archetype Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
var chunkFormatter = formatterResolver.GetFormatter<Chunk>() as ChunkFormatter;
|
||||
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
// Types
|
||||
reader.ReadPropertyName();
|
||||
var types = JsonSerializer.Deserialize<Signature>(ref reader, formatterResolver);
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Archetype lookup array
|
||||
reader.ReadPropertyName();
|
||||
var lookupArray = JsonSerializer.Deserialize<int[]>(ref reader, formatterResolver);
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Archetype chunk size and list
|
||||
reader.ReadPropertyName();
|
||||
var chunkCount = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Create archetype
|
||||
var chunks = new List<Chunk>((int)chunkCount);
|
||||
var archetype = DangerousArchetypeExtensions.CreateArchetype(World.BaseChunkSize, World.BaseChunkEntityCount, types);
|
||||
archetype.Chunks.Clear(true);
|
||||
archetype.SetCount((int)chunkCount - 1);
|
||||
|
||||
// Pass types and lookup array to the chunk formatter for saving performance and memory
|
||||
chunkFormatter!.World = World;
|
||||
chunkFormatter.Archetype = archetype;
|
||||
chunkFormatter.Signature = types;
|
||||
chunkFormatter.LookupArray = lookupArray;
|
||||
|
||||
// Deserialise each chunk and put it into the archetype.
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
|
||||
var entities = 0;
|
||||
for (var index = 0; index < chunkCount; index++)
|
||||
{
|
||||
var chunk = chunkFormatter.Deserialize(ref reader, formatterResolver);
|
||||
chunks.Add(chunk);
|
||||
entities += chunk.Count;
|
||||
reader.ReadIsValueSeparator();
|
||||
}
|
||||
|
||||
archetype.SetChunks(chunks);
|
||||
archetype.SetEntities(entities);
|
||||
|
||||
reader.ReadIsEndArray();
|
||||
reader.ReadIsEndObject();
|
||||
return archetype;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ChunkFormatter"/> class
|
||||
/// is a <see cref="IJsonFormatter{Chunk}"/> to (de)serialize <see cref="Chunk"/>s to or from json.
|
||||
/// </summary>
|
||||
public partial class ChunkFormatter : IJsonFormatter<Chunk>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Archetype"/> the current (de)serialized <see cref="Chunk"/> belongs to.
|
||||
/// Since chunks do not know this, we need to pass this information along it.
|
||||
/// </summary>
|
||||
internal World World { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Archetype"/> the current (de)serialized <see cref="Chunk"/> belongs to.
|
||||
/// Since chunks do not know this, we need to pass this information along it.
|
||||
/// </summary>
|
||||
internal Archetype Archetype { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The types used in the <see cref="Chunk"/> in each <see cref="Chunk"/> (de)serialized by this formatter.
|
||||
/// <remarks>Since <see cref="Chunk"/> does not have a reference to them and its controlled by its <see cref="Archetype"/>.</remarks>
|
||||
/// </summary>
|
||||
internal Signature Signature { get; set; } = Signature.Null;
|
||||
|
||||
/// <summary>
|
||||
/// The lookup array used by each <see cref="Chunk"/> (de)serialized by this formatter.
|
||||
/// <remarks>Since <see cref="Chunk"/> does not have a reference to them and its controlled by its <see cref="Archetype"/>.</remarks>
|
||||
/// </summary>
|
||||
internal int[] LookupArray { get; set; } = Array.Empty<int>();
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Serialize"/>
|
||||
public void Serialize(ref JsonWriter writer, Chunk value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
|
||||
// Write size
|
||||
writer.WritePropertyName("count");
|
||||
writer.WriteUInt32((uint)value.Count);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write capacity
|
||||
writer.WritePropertyName("capacity");
|
||||
writer.WriteUInt32((uint)value.Capacity);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Write entitys
|
||||
writer.WritePropertyName("entities");
|
||||
JsonSerializer.NonGeneric.Serialize(ref writer, value.Entities, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
|
||||
// Persist arrays as an array...
|
||||
writer.WritePropertyName("arrays");
|
||||
writer.WriteBeginArray();
|
||||
foreach (var type in Signature.Components)
|
||||
{
|
||||
// Write array itself
|
||||
var array = value.GetArray(type);
|
||||
JsonSerializer.Serialize(ref writer, array, formatterResolver);
|
||||
writer.WriteValueSeparator();
|
||||
}
|
||||
|
||||
// Remove trailing
|
||||
if (Signature.Count > 0)
|
||||
{
|
||||
writer.AdvanceOffset(-1);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IJsonFormatter{T}.Deserialize"/>
|
||||
public Chunk Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
|
||||
// Read chunk size
|
||||
reader.ReadPropertyName();
|
||||
var size = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Read chunk size
|
||||
reader.ReadPropertyName();
|
||||
var capacity = reader.ReadUInt32();
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Read entities
|
||||
reader.ReadPropertyName();
|
||||
var entities = JsonSerializer.Deserialize<Entity[]>(ref reader, formatterResolver);
|
||||
reader.ReadIsValueSeparator();
|
||||
|
||||
// Create chunk
|
||||
var chunk = DangerousChunkExtensions.CreateChunk((int)capacity, LookupArray, Signature);
|
||||
entities.CopyTo(chunk.Entities, 0);
|
||||
chunk.SetSize((int)size);
|
||||
|
||||
// Updating World.EntityInfoStorage to their new archetype
|
||||
for (var index = 0; index < size; index++)
|
||||
{
|
||||
ref var entity = ref chunk.Entity(index);
|
||||
entity = DangerousEntityExtensions.CreateEntityStruct(entity.Id, World.Id, entity.Version);
|
||||
World.SetArchetype(entity, Archetype);
|
||||
}
|
||||
|
||||
// Persist arrays as an array...
|
||||
reader.ReadPropertyName();
|
||||
reader.ReadIsBeginArray();
|
||||
foreach (var type in Signature)
|
||||
{
|
||||
// Read array of the type
|
||||
var array = JsonSerializer.Deserialize<Array>(ref reader, formatterResolver);
|
||||
var chunkArray = chunk.GetArray(array.GetType().GetElementType()!);
|
||||
Array.Copy(array, chunkArray, (int)size);
|
||||
reader.ReadIsValueSeparator();
|
||||
}
|
||||
|
||||
reader.ReadIsEndArray();
|
||||
reader.ReadIsEndObject();
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
410
src/Arch.Extended/Arch.Persistence/Serializer.cs
Normal file
410
src/Arch.Extended/Arch.Persistence/Serializer.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
namespace Arch.Persistence;
|
||||
|
||||
using Arch.Core;
|
||||
using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
using global::System;
|
||||
using global::System.Buffers;
|
||||
using global::System.IO;
|
||||
using Utf8Json;
|
||||
using Utf8Json.Resolvers;
|
||||
using DateTimeFormatter = Utf8Json.Formatters.DateTimeFormatter;
|
||||
using NullableDateTimeFormatter = Utf8Json.Formatters.NullableDateTimeFormatter;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IArchSerializer"/> interface
|
||||
/// represents an interface with shared methods to (de)serialize worlds and entities.
|
||||
/// <remarks>It might happen that the serialized object is too large to fit into a regular c# byte-array. In this case use the <see cref="IBufferWriter{T}"/>-API.</remarks>
|
||||
/// </summary>
|
||||
public interface IArchSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializes an <see cref="Entity"/> to a <see cref="byte"/>-array.
|
||||
/// </summary>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
byte[] Serialize(World world, Entity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an <see cref="Entity"/> to a <see cref="Stream"/> e.g. a File or existing array.
|
||||
/// </summary>
|
||||
/// <param name="stream">The <see cref="Stream"/>.</param>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
void Serialize(Stream stream, World world, Entity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an <see cref="Entity"/> to a <see cref="IBufferWriter{T}"/> e.g. a File or existing array.
|
||||
/// </summary>
|
||||
/// <param name="writer">The <see cref="IBufferWriter{T}"/>.</param>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
void Serialize(IBufferWriter<byte> writer, World world, Entity entity);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes an <see cref="Entity"/> from its bytes to an real <see cref="Entity"/> in a <see cref="World"/>.
|
||||
/// <remarks>The new <see cref="Entity.Id"/> and <see cref="Entity.WorldId"/> will differ.</remarks>
|
||||
/// </summary>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
/// <returns></returns>
|
||||
Entity Deserialize(World world, byte[] entity);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes an <see cref="Entity"/> from its bytes to an real <see cref="Entity"/> in a <see cref="World"/>.
|
||||
/// <remarks>The new <see cref="Entity.Id"/> and <see cref="Entity.WorldId"/> will differ.</remarks>
|
||||
/// </summary>
|
||||
/// <param name="stream">The <see cref="Stream"/>.</param>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
/// <returns></returns>
|
||||
Entity Deserialize(Stream stream, World world);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a <see cref="World"/> to a <see cref="byte"/>-array.
|
||||
/// </summary>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
byte[] Serialize(World world);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a <see cref="World"/> to a <see cref="Stream"/>.
|
||||
/// </summary>
|
||||
/// <param name="stream">The <see cref="Stream"/>.</param>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
void Serialize(Stream stream, World world);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a <see cref="World"/> to a <see cref="IBufferWriter{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="writer">The <see cref="IBufferWriter{T}"/>.</param>
|
||||
/// <param name="world">The <see cref="World"/>.</param>
|
||||
void Serialize(IBufferWriter<byte> writer, World world);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a byte-array into a <see cref="World"/>.
|
||||
/// </summary>
|
||||
/// <param name="world">The <see cref="World"/> as an byte-array.</param>
|
||||
/// <returns>The new <see cref="World"/>.</returns>
|
||||
World Deserialize(byte[] world);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes a byte-array into a <see cref="World"/>.
|
||||
/// </summary>
|
||||
/// <param name="stream">The <see cref="Stream"/>.</param>
|
||||
/// <returns>The new <see cref="World"/>.</returns>
|
||||
World Deserialize(Stream stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArchBinarySerializer"/> class
|
||||
/// represents a binary serializer for arch to (de)serialize single entities and whole worlds by binary.
|
||||
/// </summary>
|
||||
public class ArchBinarySerializer : IArchSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// The default formatters used to (de)serialize the <see cref="World"/>.
|
||||
/// </summary>
|
||||
private readonly IMessagePackFormatter[] _formatters =
|
||||
[
|
||||
new WorldFormatter(),
|
||||
new ArchetypeFormatter(),
|
||||
new ChunkFormatter(),
|
||||
new ArrayFormatter(),
|
||||
new ComponentTypeFormatter(),
|
||||
new SignatureFormatter(),
|
||||
new EntitySlotFormatter(),
|
||||
new EntityFormatter(),
|
||||
new JaggedArrayFormatter<int>(-1),
|
||||
new JaggedArrayFormatter<(int,int)>((-1,-1)),
|
||||
new JaggedArrayFormatter<EntityData>(new EntityData(null!, new Slot(-1,-1), -1))
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The default formatters used to (de)serialize a single <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
private readonly IMessagePackFormatter[] _singleEntityFormatters =
|
||||
[
|
||||
new ComponentTypeFormatter(),
|
||||
new SignatureFormatter(),
|
||||
new SingleEntityFormatter()
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The standard <see cref="MessagePackSerializerOptions"/> for world (de)serialization.
|
||||
/// </summary>
|
||||
private readonly MessagePackSerializerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// The standard <see cref="MessagePackSerializerOptions"/> for single entity (de)serialization.
|
||||
/// </summary>
|
||||
private readonly MessagePackSerializerOptions _singleEntityOptions;
|
||||
|
||||
/// <summary>
|
||||
/// The static constructor gets called during compile time to setup the serializer.
|
||||
/// </summary>
|
||||
public ArchBinarySerializer(params IMessagePackFormatter[] custFormatters)
|
||||
{
|
||||
// Register all important jsonformatters
|
||||
_options = MessagePackSerializerOptions.Standard.WithResolver(
|
||||
MessagePack.Resolvers.CompositeResolver.Create(
|
||||
[.. _formatters, .. custFormatters],
|
||||
[
|
||||
MessagePack.Resolvers.BuiltinResolver.Instance,
|
||||
MessagePack.Resolvers.ContractlessStandardResolverAllowPrivate.Instance
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
_singleEntityOptions = MessagePackSerializerOptions.Standard.WithResolver(
|
||||
MessagePack.Resolvers.CompositeResolver.Create(
|
||||
[.. _singleEntityFormatters, .. custFormatters],
|
||||
[
|
||||
MessagePack.Resolvers.BuiltinResolver.Instance,
|
||||
MessagePack.Resolvers.ContractlessStandardResolverAllowPrivate.Instance
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Serialize(World world, Entity entity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return MessagePackSerializer.Serialize(entity, _singleEntityOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(Stream stream, World world, Entity entity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
MessagePackSerializer.Serialize(stream, entity, _singleEntityOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(IBufferWriter<byte> writer, World world, Entity entity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
MessagePackSerializer.Serialize(writer, entity, _singleEntityOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Entity Deserialize(World world, byte[] entity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return MessagePackSerializer.Deserialize<Entity>(entity, _singleEntityOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Entity Deserialize(Stream stream, World world)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return MessagePackSerializer.Deserialize<Entity>(stream, _singleEntityOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Serialize(World world)
|
||||
{
|
||||
return MessagePackSerializer.Serialize(world, _options);
|
||||
;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(Stream stream, World world) => MessagePackSerializer.Serialize(stream, world, _options);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(IBufferWriter<byte> writer, World world) => MessagePackSerializer.Serialize(writer, world, _options);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public World Deserialize(byte[] world) => MessagePackSerializer.Deserialize<World>(world, _options);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public World Deserialize(Stream stream) => MessagePackSerializer.Deserialize<World>(stream, _options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ArchJsonSerializer"/> class
|
||||
/// represents a json serializer for arch to (de)serialize single entities and whole worlds by binary.
|
||||
/// </summary>
|
||||
public class ArchJsonSerializer : IArchSerializer
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The default formatters used to (de)serialize the <see cref="World"/>.
|
||||
/// </summary>
|
||||
private readonly IJsonFormatter[] _formatters = [
|
||||
new WorldFormatter(),
|
||||
new ArchetypeFormatter(),
|
||||
new ChunkFormatter(),
|
||||
new ArrayFormatter(),
|
||||
new ComponentTypeFormatter(),
|
||||
new SignatureFormatter(),
|
||||
new EntitySlotFormatter(),
|
||||
new EntityFormatter(),
|
||||
new JaggedArrayFormatter<int>(-1),
|
||||
new JaggedArrayFormatter<(int,int)>((-1,-1)),
|
||||
new JaggedArrayFormatter<EntityData>(new EntityData(null!, new Slot(-1, -1), -1)),
|
||||
new DateTimeFormatter("yyyy-MM-dd HH:mm:ss"),
|
||||
new NullableDateTimeFormatter("yyyy-MM-dd HH:mm:ss")
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The default formatters used to (de)serialize a single <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
private readonly IJsonFormatter[] _singleEntityFormatters =
|
||||
[
|
||||
new ComponentTypeFormatter(),
|
||||
new SignatureFormatter(),
|
||||
new SingleEntityFormatter(),
|
||||
new DateTimeFormatter("yyyy-MM-dd HH:mm:ss"),
|
||||
new NullableDateTimeFormatter("yyyy-MM-dd HH:mm:ss")
|
||||
];
|
||||
|
||||
// It can `not` garbage collect and create is slightly high cost.
|
||||
// so you should store to static field.
|
||||
private readonly IJsonFormatterResolver _formatterResolver;
|
||||
|
||||
// CompositeResolver.Create can create dynamic composite resolver.
|
||||
// It can `not` garbage collect and create is slightly high cost.
|
||||
// so you should store to static field.
|
||||
private readonly IJsonFormatterResolver _singleEntityFormatterResolver;
|
||||
|
||||
/// <summary>
|
||||
/// The static constructor gets called during compile time to setup the serializer.
|
||||
/// </summary>
|
||||
public ArchJsonSerializer(params IJsonFormatter[] custFormatters)
|
||||
{
|
||||
// Register all important jsonformatters
|
||||
_formatterResolver = CompositeResolver.Create(
|
||||
[.. _formatters, .. custFormatters],
|
||||
[
|
||||
EnumResolver.UnderlyingValue,
|
||||
StandardResolver.AllowPrivateExcludeNullSnakeCase,
|
||||
BuiltinResolver.Instance,
|
||||
DynamicGenericResolver.Instance,
|
||||
]
|
||||
);
|
||||
|
||||
_singleEntityFormatterResolver = CompositeResolver.Create(
|
||||
[.. _singleEntityFormatters, .. custFormatters],
|
||||
[
|
||||
EnumResolver.UnderlyingValue,
|
||||
StandardResolver.AllowPrivateExcludeNullSnakeCase,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The static constructor gets called during compile time to setup the serializer.
|
||||
/// This variant allows custom resolvers to be passed in as well.
|
||||
/// </summary>
|
||||
public ArchJsonSerializer(IJsonFormatter[] custFormatters, IJsonFormatterResolver[] custResolvers)
|
||||
{
|
||||
// Register all important jsonformatters
|
||||
_formatterResolver = CompositeResolver.Create(
|
||||
[.. _formatters, .. custFormatters],
|
||||
[
|
||||
.. custResolvers,
|
||||
.. new[] {
|
||||
EnumResolver.UnderlyingValue,
|
||||
StandardResolver.AllowPrivateExcludeNullSnakeCase,
|
||||
BuiltinResolver.Instance,
|
||||
DynamicGenericResolver.Instance,
|
||||
},
|
||||
]);
|
||||
|
||||
_singleEntityFormatterResolver = CompositeResolver.Create(
|
||||
[.. _singleEntityFormatters, .. custFormatters],
|
||||
[
|
||||
.. custResolvers,
|
||||
.. new[] {
|
||||
EnumResolver.UnderlyingValue,
|
||||
StandardResolver.AllowPrivateExcludeNullSnakeCase,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the given <see cref="World"/> to a json-string.
|
||||
/// </summary>
|
||||
/// <param name="world">The <see cref="World"/> to serialize.</param>
|
||||
/// <returns>Its json-string.</returns>
|
||||
public string ToJson(World world) => JsonSerializer.ToJsonString(world, _formatterResolver);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the given <see cref="Entity"/> to a json-string.
|
||||
/// </summary>
|
||||
/// <param name="world">The <see cref="World"/> the entity belongs to..</param>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
/// <returns>Its json-string.</returns>
|
||||
/// <returns>A json-string of the entity with all its components.</returns>
|
||||
public string ToJson(World world, Entity entity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return JsonSerializer.ToJsonString(entity, _singleEntityFormatterResolver);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the given json <see cref="string"/> to a <see cref="World"/>.
|
||||
/// </summary>
|
||||
/// <param name="jsonWorld">The json <see cref="string"/> to deserialize.</param>
|
||||
/// <returns>A new <see cref="World"/>.</returns>
|
||||
public World FromJson(string jsonWorld) => JsonSerializer.Deserialize<World>(jsonWorld, _formatterResolver);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the given json <see cref="string"/> to a <see cref="World"/>.
|
||||
/// <remarks>The deserialized <see cref="Entity"/> will receive a new id and a new worldId.</remarks>
|
||||
/// </summary>
|
||||
/// <param name="world">The <see cref="World"/> to deserialize the entity into.</param>
|
||||
/// <param name="jsonEntity">The json <see cref="string"/> of the entity to deserialize.</param>
|
||||
/// <returns>A new <see cref="Entity"/>.</returns>
|
||||
public Entity FromJson(World world, string jsonEntity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return JsonSerializer.Deserialize<Entity>(jsonEntity, _singleEntityFormatterResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Serialize(World world, Entity entity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return JsonSerializer.Serialize(entity, _singleEntityFormatterResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(Stream stream, World world, Entity entity)
|
||||
{
|
||||
(_singleEntityFormatters[1] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
JsonSerializer.Serialize(stream, entity, _singleEntityFormatterResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(IBufferWriter<byte> writer, World world, Entity entity) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Entity Deserialize(World world, byte[] entity)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return JsonSerializer.Deserialize<Entity>(entity, _singleEntityFormatterResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Entity Deserialize(Stream stream, World world)
|
||||
{
|
||||
(_singleEntityFormatters[2] as SingleEntityFormatter)!.EntityWorld = world;
|
||||
return JsonSerializer.Deserialize<Entity>(stream, _singleEntityFormatterResolver);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public byte[] Serialize(World world) => JsonSerializer.Serialize(world, _formatterResolver);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(Stream stream, World world) => JsonSerializer.Serialize(stream, world, _formatterResolver);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Serialize(IBufferWriter<byte> writer, World world) => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public World Deserialize(byte[] world) => JsonSerializer.Deserialize<World>(world, _formatterResolver);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public World Deserialize(Stream stream) => JsonSerializer.Deserialize<World>(stream, _formatterResolver);
|
||||
}
|
||||
139
src/Arch.Extended/Arch.Persistence/StreamBufferWriter.cs
Normal file
139
src/Arch.Extended/Arch.Persistence/StreamBufferWriter.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.IO;
|
||||
|
||||
namespace Arch.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="StreamBufferWriter"/> class
|
||||
/// is a small wrapper around a <see cref="Stream"/> implementing a <see cref="IBufferWriter{T}"/>.
|
||||
/// It buffers incoming bytes in an internally stored array and flushes it regulary into the <see cref="_destination"/>-<see cref="Stream"/>.
|
||||
/// </summary>
|
||||
public sealed class StreamBufferWriter : IBufferWriter<byte>, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The buffer.
|
||||
/// </summary>
|
||||
private byte[] _buffer;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Stream"/>.
|
||||
/// </summary>
|
||||
private readonly Stream _destination;
|
||||
|
||||
/// <summary>
|
||||
// / If this instance owns the <see cref="_destination"/> stream.
|
||||
/// </summary>
|
||||
private readonly bool _ownsStream;
|
||||
|
||||
/// <summary>
|
||||
/// The current position and the amount of total leased bytes.
|
||||
/// </summary>
|
||||
private int _position, _leased;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="StreamBufferWriter"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="destination">The <see cref="Stream"/>.</param>
|
||||
/// <param name="bufferSize">The buffer-size of the <see cref="_buffer"/>.</param>
|
||||
/// <param name="ownsStream">If it owns the stream.</param>
|
||||
public StreamBufferWriter(Stream destination, int bufferSize = 1024, bool ownsStream = true)
|
||||
{
|
||||
const int minBufferSize = 128;
|
||||
if (bufferSize < minBufferSize)
|
||||
{
|
||||
bufferSize = minBufferSize;
|
||||
}
|
||||
|
||||
_buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
||||
_ownsStream = ownsStream;
|
||||
_destination = destination;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Leases an amount of bytes from the <see cref="_buffer"/>.
|
||||
/// </summary>
|
||||
/// <param name="sizeHint">The total amount.</param>
|
||||
/// <returns>The leased amount.</returns>
|
||||
private int Lease(int sizeHint)
|
||||
{
|
||||
var available = _buffer.Length - _position;
|
||||
if (available < sizeHint && _position != 0)
|
||||
{ // try to get more
|
||||
Flush();
|
||||
available = _buffer.Length - _position;
|
||||
}
|
||||
|
||||
_leased = available;
|
||||
return available;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes the buffered bytes to the <see cref="_destination"/>.
|
||||
/// </summary>
|
||||
/// <param name="flushUnderlyingStream">If it also should flush the <see cref="Stream"/>.</param>
|
||||
public void Flush(bool flushUnderlyingStream = false)
|
||||
{
|
||||
if (_position != 0)
|
||||
{
|
||||
_destination.Write(_buffer, 0, _position);
|
||||
_position = 0;
|
||||
}
|
||||
if (flushUnderlyingStream)
|
||||
{
|
||||
_destination.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advances the buffer, notifies this instance that there was something new written into the <see cref="_buffer"/> memory.
|
||||
/// </summary>
|
||||
/// <param name="count">The amount of bytes written.</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Throws if we are out of memory.</exception>
|
||||
void IBufferWriter<byte>.Advance(int count)
|
||||
{
|
||||
if (count > _leased || count < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
_position += count;
|
||||
_leased = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a partion of the <see cref="_buffer"/> as a <see cref="Memory{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="sizeHint">The total amount.</param>
|
||||
/// <returns>The new <see cref="Memory{T}"/> instance.</returns>
|
||||
Memory<byte> IBufferWriter<byte>.GetMemory(int sizeHint)
|
||||
{
|
||||
var actual = Lease(sizeHint);
|
||||
return new Memory<byte>(_buffer, _position, actual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a partion of the <see cref="_buffer"/> as a <see cref="Span{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="sizeHint">The total amount.</param>
|
||||
/// <returns>The new <see cref="Span{T}"/> instance.</returns>
|
||||
Span<byte> IBufferWriter<byte>.GetSpan(int sizeHint)
|
||||
{
|
||||
var actual = Lease(sizeHint);
|
||||
return new Span<byte>(_buffer, _position, actual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this instance, flushes and releases all memory.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Flush(true);
|
||||
|
||||
var tmp = _buffer;
|
||||
_buffer = null!;
|
||||
ArrayPool<byte>.Shared.Return(tmp);
|
||||
|
||||
if (_ownsStream)
|
||||
{
|
||||
_destination.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
namespace Arch.Relationships;
|
||||
|
||||
using global::System.Diagnostics.Contracts;
|
||||
using global::System.Runtime.CompilerServices;
|
||||
using Arch.Core;
|
||||
|
||||
#if !PURE_ECS
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="EntityRelationshipExtensions"/> class
|
||||
/// stores several methods to forward relationship methods from the <see cref="World"/> to the <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
public static class EntityRelationshipExtensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new relationship to the <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
// / <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="relationship">The relationship instance.</param>
|
||||
public static void AddRelationship<T>(this in Entity source, Entity target, T relationship = default!)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
world.AddRelationship(source, target, relationship);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a relationship to the <see cref="Entity"/> by updating its relationship data.
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="relationship">The relationship instance.</param>
|
||||
public static void SetRelationship<T>(this in Entity source, Entity target, T relationship = default!)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
world.SetRelationship(source, target, relationship);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an <see cref="Entity"/> has a certain relationship.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>True if it has the desired relationship, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static bool HasRelationship<T>(this in Entity source, Entity target)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
return world.HasRelationship<T>(source, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an <see cref="Entity"/> has a certain relationship.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>True if it has the desired relationship, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static bool HasRelationship<T>(this in Entity source)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
return world.HasRelationship<T>(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a relationship of an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>The relationship.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static T GetRelationship<T>(this in Entity source, Entity target)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
return world.GetRelationship<T>(source, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a relationship of an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>The <see cref="Relationship{T}"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static ref Relationship<T> GetRelationships<T>(this in Entity source)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
return ref world.GetRelationships<T>(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to return an <see cref="Entity"/>s relationship of the specified type.
|
||||
/// Will copy the relationship if its a struct.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="relationship">The found relationship.</param>
|
||||
/// <returns>True if it exists, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static bool TryGetRelationship<T>(this in Entity source, Entity target, out T relationship)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
return world.TryGetRelationship(source, target, out relationship);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a relationship from an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="source">The <see cref="Entity"/> to remove the relationship from.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
public static void RemoveRelationship<T>(this in Entity source, Entity target)
|
||||
{
|
||||
var world = World.Worlds[source.WorldId];
|
||||
world.RemoveRelationship<T>(source, target);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1 @@
|
||||
uid://574q5d6c3s6y
|
||||
58
src/Arch.Extended/Arch.Relationships/Enumerators.cs
Normal file
58
src/Arch.Extended/Arch.Relationships/Enumerators.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace Arch.Relationships;
|
||||
|
||||
using global::System;
|
||||
using global::System.Collections.Generic;
|
||||
using Arch.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SortedListEnumerator{TValue}"/> struct
|
||||
/// is a enumerator to enumerate a passed <see cref="SortedList{TKey,TValue}"/> in an efficient way.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
public struct SortedListEnumerator<TValue>
|
||||
{
|
||||
private readonly SortedList<Entity, TValue> _sortedList;
|
||||
private int _currentIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor.
|
||||
/// </summary>
|
||||
/// <param name="list">List.</param>
|
||||
public SortedListEnumerator(SortedList<Entity, TValue> list)
|
||||
{
|
||||
_sortedList = list;
|
||||
_currentIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current.
|
||||
/// </summary>
|
||||
public readonly KeyValuePair<Entity, TValue> Current
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_currentIndex == -1 || _currentIndex >= _sortedList.Count)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
var key = _sortedList.Keys[_currentIndex];
|
||||
var value = _sortedList.Values[_currentIndex];
|
||||
return new KeyValuePair<Entity, TValue>(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves to the next element in the enumerator.
|
||||
/// </summary>
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentIndex++;
|
||||
return _currentIndex < _sortedList.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the enumerator to its initial position.
|
||||
/// </summary>
|
||||
public void Reset() => _currentIndex = -1;
|
||||
}
|
||||
1
src/Arch.Extended/Arch.Relationships/Enumerators.cs.uid
Normal file
1
src/Arch.Extended/Arch.Relationships/Enumerators.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://br3hmo40b0kmx
|
||||
38
src/Arch.Extended/Arch.Relationships/InRelationship.cs
Normal file
38
src/Arch.Extended/Arch.Relationships/InRelationship.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
namespace Arch.Relationships;
|
||||
|
||||
using Arch.Core;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The struct <see cref="InRelationship"/>
|
||||
/// represents a reference to a <see cref="Relationship{T}"/>.
|
||||
/// It sits on an <see cref="Entity"/> to indicate in which other <see cref="Relationship{T}"/>s it is involved in.
|
||||
/// </summary>
|
||||
internal readonly struct InRelationship
|
||||
{
|
||||
/// <summary>
|
||||
/// The id of the <see cref="Relationship{T}"/>-Component that this <see cref="InRelationship"/> points to.
|
||||
/// Basically the <see cref="Relationship{T}"/> the <see cref="Entity"/> is in.
|
||||
/// TODO: Uhmm... how the heck do we convert the Id back to the <see cref="ComponentType"/>?
|
||||
/// </summary>
|
||||
public readonly int ComponentTypeId;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="InRelationship"/> instance.
|
||||
/// </summary>
|
||||
/// <param name="targetRelation">The <see cref="ComponentType"/> that represents the relation.</param>
|
||||
internal InRelationship(ComponentType targetRelation)
|
||||
{
|
||||
ComponentTypeId = targetRelation.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="InRelationship"/> instance.
|
||||
/// <remarks>Mostly for binary serialization.</remarks>
|
||||
/// </summary>
|
||||
/// <param name="componentTypeId">The <see cref="ComponentTypeId"/>.</param>
|
||||
internal InRelationship(int componentTypeId)
|
||||
{
|
||||
ComponentTypeId = componentTypeId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cu62xtv73hjq7
|
||||
146
src/Arch.Extended/Arch.Relationships/Relationship.cs
Normal file
146
src/Arch.Extended/Arch.Relationships/Relationship.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
namespace Arch.Relationships;
|
||||
|
||||
using global::System.Collections.Generic;
|
||||
using global::System.Runtime.CompilerServices;
|
||||
using Arch.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IRelationship"/> interface
|
||||
/// is an interface that provides all methods required to act as a relationship.
|
||||
/// </summary>
|
||||
internal interface IRelationship
|
||||
{
|
||||
/// <summary>
|
||||
/// The amount of relationships currently in the buffer.
|
||||
/// </summary>
|
||||
int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the buffer as a component from the given world and entity.
|
||||
/// </summary>
|
||||
/// <param name="world"></param>
|
||||
/// <param name="source"></param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void Destroy(World world, Entity source);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the relationship targeting <paramref name="target"/> from this buffer.
|
||||
/// </summary>
|
||||
/// <param name="target">The <see cref="Entity"/> in the relationship to remove.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void Remove(Entity target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A buffer storing relationships of <see cref="Entity"/> and <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the second relationship element.</typeparam>
|
||||
public class Relationship<T> : IRelationship
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Its relations.
|
||||
/// </summary>
|
||||
internal readonly SortedList<Entity, T> _elements;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of an <see cref="Relationship{T}"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal Relationship()
|
||||
{
|
||||
_elements = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of an <see cref="Relationship{T}"/>.
|
||||
/// <remarks>Mostly for binary serialization.</remarks>
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal Relationship(SortedList<Entity, T> elements)
|
||||
{
|
||||
_elements = elements;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
int IRelationship.Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _elements.Count;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IRelationship.Count"/>
|
||||
internal int Count
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => ((IRelationship)this).Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a relationship to this buffer.
|
||||
/// </summary>
|
||||
/// <param name="relationship">The instance of the relationship.</param>
|
||||
/// <param name="target">The target of the relationship.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void Add(in T relationship, Entity target) => _elements.Add(target, relationship);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the stored <typeparamref name="T"/> for the given <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
/// <param name="data">The data to store.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Set(Entity entity, T data = default!) => _elements[entity] = data;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the given <see cref="Relationship{T}"/> contains the passed <see cref="Entity"/> or not.
|
||||
/// </summary>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
/// <returns>True or false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Contains(Entity entity) => _elements.ContainsKey(entity);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stored <typeparamref name="T"/> for the given <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
/// <returns>The stored <typeparamref name="T"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public T Get(Entity entity) => _elements[entity];
|
||||
|
||||
/// <summary>
|
||||
/// Returns the stored <typeparamref name="T"/> for the given <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The <see cref="Entity"/>.</param>
|
||||
/// <param name="value">The stored <typeparamref name="T"/>.</param>
|
||||
/// <returns>The stored <typeparamref name="T"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetValue(Entity entity, out T value) => _elements.TryGetValue(entity, out value!);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void IRelationship.Remove(Entity target) => _elements.Remove(target);
|
||||
|
||||
/// <inheritdoc cref="IRelationship.Remove(Entity)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void Remove(Entity target) => ((IRelationship)this).Remove(target);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void IRelationship.Destroy(World world, Entity source) => world.Remove<Relationship<T>>(source);
|
||||
|
||||
/// <inheritdoc cref="IRelationship.Destroy(World, Entity)"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void Destroy(World world, Entity source) => ((IRelationship)this).Destroy(world, source);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="SortedListEnumerator{TValue}"/>.
|
||||
/// </summary>
|
||||
/// <returns>The new <see cref="SortedListEnumerator{TValue}"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SortedListEnumerator<T> GetEnumerator() => new(_elements);
|
||||
};
|
||||
1
src/Arch.Extended/Arch.Relationships/Relationship.cs.uid
Normal file
1
src/Arch.Extended/Arch.Relationships/Relationship.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d2oi4itpntwl3
|
||||
@@ -0,0 +1,272 @@
|
||||
// [assembly:InternalsVisibleTo("Arch.Relationships.Tests")]
|
||||
namespace Arch.Relationships;
|
||||
|
||||
using global::System.Diagnostics.Contracts;
|
||||
using global::System.Runtime.CompilerServices;
|
||||
using Arch.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="WorldRelationshipExtensions"/> class
|
||||
/// stores several extension methods for relationships handling.
|
||||
/// </summary>
|
||||
public static class WorldRelationshipExtensions
|
||||
{
|
||||
|
||||
#if EVENTS
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to entity destruction events to cleanup their relations.
|
||||
/// </summary>
|
||||
public static void HandleRelationshipCleanup(this World world)
|
||||
{
|
||||
world.SubscribeEntityDestroyed((in Entity entity) => CleanupRelationships(world, in entity));
|
||||
}
|
||||
|
||||
// TODO: Probably someone will kill me for the dark magic that happens down below.
|
||||
/// <summary>
|
||||
/// Cleans up all relations of the passed <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="world"></param>
|
||||
/// <param name="entity"></param>
|
||||
public static void CleanupRelationships(this World world, in Entity entity)
|
||||
{
|
||||
ref var relationships = ref world.TryGetRefRelationships<InRelationship>(entity, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (target, inRelationship) in relationships.Elements)
|
||||
{
|
||||
var id = inRelationship.ComponentTypeId;
|
||||
var componentType = new ComponentType(id, 0);
|
||||
|
||||
// Get slots, chunk and array to prevent entity.Get(type) object allocation
|
||||
ref readonly var chunk = ref world.GetChunk(target);
|
||||
var array = chunk.GetArray(componentType);
|
||||
var relationshipsArray = Unsafe.As<IRelationship[]>(array);
|
||||
|
||||
var slot = world.GetSlot(target);
|
||||
var relationship = relationshipsArray[slot.Index];
|
||||
relationship.Remove(entity);
|
||||
|
||||
if (relationship.Count == 0)
|
||||
{
|
||||
relationship.Destroy(world, target);
|
||||
}
|
||||
|
||||
ref var targetRelationships = ref world.TryGetRefRelationships<InRelationship>(target, out exists);
|
||||
if (!exists)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
targetRelationships.Remove(entity);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new relationship to the <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="relationship">The relationship instance.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void AddRelationship<T>(this World world, Entity source, Entity target, in T relationship = default!)
|
||||
{
|
||||
ref var buffer = ref world.AddOrGetRelationships<T>(source);
|
||||
buffer.Add(in relationship, target);
|
||||
|
||||
var targetComp = new InRelationship(Component<Relationship<T>>.ComponentType);
|
||||
ref var targetBuffer = ref world.AddOrGetRelationships<InRelationship>(target);
|
||||
targetBuffer.Add(in targetComp, source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the existence of a relationship on an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="relationship">The relationship value used if its being added.</param>
|
||||
/// <returns>The relationship.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static T AddOrGetRelationship<T>(this World world, Entity source, Entity target, in T relationship = default!)
|
||||
{
|
||||
ref var relationships = ref world.TryGetRefRelationships<T>(source, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
return relationships.Get(target);
|
||||
}
|
||||
|
||||
world.AddRelationship(source, target, in relationship);
|
||||
return world.GetRelationship<T>(source, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the existence of a buffer of relationships on an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationships.</param>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <returns>The relationships.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static ref Relationship<T> AddOrGetRelationships<T>(this World world, Entity source)
|
||||
{
|
||||
ref var component = ref world.TryGetRef<Relationship<T>>(source, out var exists);
|
||||
if (exists)
|
||||
{
|
||||
return ref component!;
|
||||
}
|
||||
|
||||
world.Add(source, new Relationship<T>());
|
||||
return ref world.Get<Relationship<T>>(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the existing relationship data.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="relationship">The new data.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetRelationship<T>(this World world, Entity source, Entity target, in T relationship = default!)
|
||||
{
|
||||
ref var relationships = ref world.GetRelationships<T>(source);
|
||||
relationships.Set(target, relationship);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an <see cref="Entity"/> has a certain relationship.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>True if it has the desired relationship, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static bool HasRelationship<T>(this World world, Entity source, Entity target)
|
||||
{
|
||||
ref var relationships = ref world.TryGetRefRelationships<T>(source, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return relationships.Contains(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an <see cref="Entity"/> has a certain relationship.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>True if it has the desired relationship, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static bool HasRelationship<T>(this World world, Entity source) => world.Has<Relationship<T>>(source);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a relationship of an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>The relationship.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static T GetRelationship<T>(this World world, Entity source, Entity target)
|
||||
{
|
||||
ref var relationships = ref world.GetRelationships<T>(source);
|
||||
return relationships.Get(target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to return an <see cref="Entity"/>s relationship of the specified type.
|
||||
/// Will copy the relationship if its a struct.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
/// <param name="relationship">The found relationship.</param>
|
||||
/// <returns>True if it exists, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static bool TryGetRelationship<T>(this World world, Entity source, Entity target, out T relationship)
|
||||
{
|
||||
ref var relationships = ref world.TryGetRefRelationships<T>(source, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
relationship = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
return relationships.TryGetValue(target, out relationship);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all relationships of the given type of an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The source <see cref="Entity"/> of the relationship.</param>
|
||||
/// <returns>A reference to the relationships.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
public static ref Relationship<T> GetRelationships<T>(this World world, Entity source) => ref world.Get<Relationship<T>>(source);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to return an <see cref="Entity"/>s relationships of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The <see cref="Entity"/>.</param>
|
||||
/// <param name="relationships">The found relationships.</param>
|
||||
/// <returns>True if it exists, otherwise false.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
internal static bool TryGetRelationships<T>(this World world, Entity source, out Relationship<T> relationships) => world.TryGet(source, out relationships!);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to return a reference to an <see cref="Entity"/>s relationships of the
|
||||
/// specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The <see cref="Entity"/>.</param>
|
||||
/// <param name="exists">True if it exists, otherwise false.</param>
|
||||
/// <returns>A reference to the relationships.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining), Pure]
|
||||
internal static ref Relationship<T> TryGetRefRelationships<T>(this World world, Entity source, out bool exists) => ref world.TryGetRef<Relationship<T>>(source, out exists);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a relationship from an <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The relationship type.</typeparam>
|
||||
/// <param name="world">World.</param>
|
||||
/// <param name="source">The <see cref="Entity"/> to remove the relationship from.</param>
|
||||
/// <param name="target">The target <see cref="Entity"/> of the relationship.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void RemoveRelationship<T>(this World world, Entity source, Entity target)
|
||||
{
|
||||
ref var buffer = ref world.GetRelationships<T>(source);
|
||||
buffer.Remove(target);
|
||||
|
||||
if (buffer.Count == 0)
|
||||
{
|
||||
world.Remove<Relationship<T>>(source);
|
||||
}
|
||||
|
||||
ref var targetBuffer = ref world.GetRelationships<InRelationship>(target);
|
||||
targetBuffer.Remove(source);
|
||||
|
||||
if (targetBuffer.Count == 0)
|
||||
{
|
||||
world.Remove<Relationship<InRelationship>>(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfnift6mppxam
|
||||
@@ -12,8 +12,8 @@ public partial class ConveyorBeltStraight : Node3D, IProvide<IVoxelGridRegistry>
|
||||
{
|
||||
public override void _Notification(int what) => this.Notify(what);
|
||||
[Export] public int Width { get; set; } = default;
|
||||
public IVoxelGridRegistry Value() => Grid;
|
||||
[Dependency] protected IVoxelGridRegistry Grid => this.DependOn<IVoxelGridRegistry>();
|
||||
public IVoxelGridRegistry Value() => FoodFactoryApi.GridRegistry;
|
||||
[Dependency] protected IFoodFactoryApi FoodFactoryApi => this.DependOn<IFoodFactoryApi>();
|
||||
private VoxelGuid _guid = default;
|
||||
public GridTransform3D VoxelTransform
|
||||
{
|
||||
@@ -62,13 +62,13 @@ public partial class ConveyorBeltStraight : Node3D, IProvide<IVoxelGridRegistry>
|
||||
}
|
||||
public void OnResolved()
|
||||
{
|
||||
_guid = Grid.Register(this, VoxelTransform.Origin);
|
||||
_guid = FoodFactoryApi.GridRegistry.Register(this, VoxelTransform.Origin);
|
||||
this.Provide();
|
||||
|
||||
}
|
||||
public override void _ExitTree()
|
||||
{
|
||||
Grid.UnRegister(_guid);
|
||||
FoodFactoryApi.GridRegistry.UnRegister(_guid);
|
||||
base._ExitTree();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[gd_scene format=3 uid="uid://c4h7mwnfrdesg"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dgyuh6un1qoj4" path="res://src/Conveyors/ConveyorBeltStraight/ConveyorBeltStraight.cs" id="1_mn8ro"]
|
||||
[ext_resource type="Script" uid="uid://j24fuotdwwx4" path="res://src/VoxelGrid/TestItemConveyor.cs" id="2_1vr1d"]
|
||||
[ext_resource type="Script" uid="uid://j24fuotdwwx4" path="res://src/Conveyors/TestItemConveyor.cs" id="2_1vr1d"]
|
||||
[ext_resource type="PackedScene" uid="uid://bapdy1c6rb4qr" path="res://assets/kenney_conveyor-kit/Models/GLB format/conveyor-sides.glb" id="2_mn8ro"]
|
||||
[ext_resource type="PackedScene" uid="uid://ccuhhy2oa8xvm" path="res://assets/kenney_conveyor-kit/Models/GLB format/arrow-basic.glb" id="3_1vr1d"]
|
||||
[ext_resource type="Script" uid="uid://bjuntmf2sjynp" path="res://src/VoxelGrid/ConveyorItemRender.cs" id="4_g7n1f"]
|
||||
[ext_resource type="Script" uid="uid://bjuntmf2sjynp" path="res://src/Conveyors/ConveyorItemRender.cs" id="4_g7n1f"]
|
||||
|
||||
[sub_resource type="Curve3D" id="Curve3D_x5qdr"]
|
||||
_data = {
|
||||
|
||||
@@ -3,10 +3,14 @@ namespace FoodFactory.Conveyors;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Arch.Core;
|
||||
using Chickensoft.AutoInject;
|
||||
using Chickensoft.Introspection;
|
||||
using Chickensoft.Sync.Primitives;
|
||||
using CommunityToolkit.HighPerformance.Helpers;
|
||||
using FoodFactory.Items;
|
||||
using Godot;
|
||||
using SJK.Math;
|
||||
|
||||
[Meta(typeof(IAutoNode))]
|
||||
public partial class ConveyorItemRender : Node
|
||||
@@ -15,48 +19,69 @@ public partial class ConveyorItemRender : Node
|
||||
[Export] protected Path3D Path3D { get; set; } = default!;
|
||||
[Export] protected TestItemConveyor ItemConveyor { get; set; } = default!;
|
||||
[Chickensoft.AutoInject.Dependency] protected IItemRenderer Items => this.DependOn<IItemRenderer>();
|
||||
[Chickensoft.AutoInject.Dependency] protected IFoodFactoryApi FoodFactoryApi => this.DependOn<IFoodFactoryApi>();
|
||||
// private Chickensoft.Sync.Primitives.AutoList<ConveyorSlice>.Binding _binding = default!;
|
||||
public override async void _Ready()
|
||||
private AutoList<Ordered1DList<IBeltItem>.Entry>.Binding _binding = default!;
|
||||
public void OnResolved()
|
||||
{
|
||||
base._Ready();
|
||||
if (!ItemConveyor.IsNodeReady())
|
||||
{
|
||||
await ToSignal(ItemConveyor, Node.SignalName.Ready);
|
||||
}
|
||||
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||
ItemConveyor.GetChildren().OfType<Timer>().First().Timeout += () => offset = 0;
|
||||
return;
|
||||
// _binding = ItemConveyor.Items.Items.Bind();
|
||||
// binding.OnRemove(callback =>
|
||||
// {
|
||||
// Items.Remove(callback.Item);
|
||||
// // GD.PrintS(callback.Item, callback.BeltT);
|
||||
// // if (itemsRenders.TryGetValue(callback.Item, out var node))
|
||||
// // {
|
||||
// // GD.PrintS(callback.Item, callback.BeltT,node);
|
||||
// // itemsRenders.Remove(callback.Item);
|
||||
// // node.QueueFree();
|
||||
// // }
|
||||
// });
|
||||
// _binding.OnAdd((i, v) => Items.UpdateTransform(i.Item, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(i.BeltT)));
|
||||
// _binding.OnUpdate((a, b) => Items.UpdateTransform(a.Item, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(b.BeltT).Translated(-Path3D.Curve.SampleBakedWithRotation(b.BeltT).Basis.X * b.LaneSpan.Start)));
|
||||
|
||||
_binding = ItemConveyor.Items.Values.Bind().OnUpdate((old, updated) =>
|
||||
{
|
||||
if (old.Position == updated.Position)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Items.UpdateTransform(updated.Value, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(updated.Position), (float)delta);
|
||||
|
||||
});
|
||||
// FoodFactoryApi.TickManger.GameTick += _ =>Process(_.Delta);// offset = 0;
|
||||
FoodFactoryApi.TickManger.GameTick += _ => delta = _.Delta;// offset = 0;
|
||||
}
|
||||
// public override async void _Ready()
|
||||
// {
|
||||
// base._Ready();
|
||||
// if (!ItemConveyor.IsNodeReady())
|
||||
// {
|
||||
// await ToSignal(ItemConveyor, Node.SignalName.Ready);
|
||||
// }
|
||||
// await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||
// // ItemConveyor.GetChildren().OfType<Timer>().First().Timeout += () => offset = 0;
|
||||
// return;
|
||||
// // _binding = ItemConveyor.Items.Items.Bind();
|
||||
// // binding.OnRemove(callback =>
|
||||
// // {
|
||||
// // Items.Remove(callback.Item);
|
||||
// // // GD.PrintS(callback.Item, callback.BeltT);
|
||||
// // // if (itemsRenders.TryGetValue(callback.Item, out var node))
|
||||
// // // {
|
||||
// // // GD.PrintS(callback.Item, callback.BeltT,node);
|
||||
// // // itemsRenders.Remove(callback.Item);
|
||||
// // // node.QueueFree();
|
||||
// // // }
|
||||
// // });
|
||||
// // _binding.OnAdd((i, v) => Items.UpdateTransform(i.Item, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(i.BeltT)));
|
||||
// // _binding.OnUpdate((a, b) => Items.UpdateTransform(a.Item, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(b.BeltT).Translated(-Path3D.Curve.SampleBakedWithRotation(b.BeltT).Basis.X * b.LaneSpan.Start)));
|
||||
|
||||
// }
|
||||
float offset = 0;
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
offset += (float)delta;
|
||||
base._Process(delta);
|
||||
var e = ItemConveyor.EnumerateTowardStart();
|
||||
while (e.MoveNext())
|
||||
{
|
||||
Items.UpdateTransform(e.Current.Value, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(e.Current.Position + (ItemConveyor.SignedSpeed * offset)));
|
||||
}
|
||||
}
|
||||
double delta = 0;
|
||||
|
||||
// public void Process(double delta)
|
||||
// {
|
||||
// offset += (float)delta;
|
||||
// base._Process(delta);
|
||||
// var e = ItemConveyor.EnumerateTowardStart();
|
||||
// while (e.MoveNext())
|
||||
// {
|
||||
// // Items.UpdateTransform(e.Current.Value, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(e.Current.Position + (ItemConveyor.SignedSpeed * offset)));
|
||||
// Items.UpdateTransform(e.Current.Value, Path3D.GlobalTransform * Path3D.Curve.SampleBakedWithRotation(e.Current.Position), (float)delta);
|
||||
// }
|
||||
// }
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
// _binding.Dispose();
|
||||
_binding.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -68,6 +93,58 @@ public interface IItemRenderer
|
||||
void UpdateTransform(IBeltItem beltItem, Transform3D newTransform, float time);
|
||||
void Remove(IBeltItem beltItem);
|
||||
}
|
||||
public partial class ItemRenderBuffered : Node3D, IItemRenderer
|
||||
{
|
||||
private record struct ItemEntry(IBeltItem Item, Transform3D PreviousTransform, Transform3D NewTransform, float Elapsed, float Duration);
|
||||
private List<ItemEntry> _current = [];
|
||||
private List<ItemEntry> _next = [];
|
||||
private Dictionary<IBeltItem, Node3D> _visuals = [];
|
||||
public void Remove(IBeltItem beltItem) => throw new System.NotImplementedException();
|
||||
public void UpdateTransform(IBeltItem beltItem, Transform3D newTransform) => throw new System.NotImplementedException();
|
||||
public void UpdateTransform(IBeltItem beltItem, Transform3D newTransform, float time)
|
||||
{
|
||||
var index = _current.FindIndex(f => f.Item == beltItem);
|
||||
if (index >= 0)
|
||||
{
|
||||
_current[index] = _current[index] with { PreviousTransform = _current[index].NewTransform, NewTransform = newTransform, Elapsed = 0, Duration = time };
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_visuals.TryGetValue(beltItem, out var node3D))
|
||||
{
|
||||
_visuals[beltItem] = node3D = beltItem.CreateItemVisual();
|
||||
node3D.Transform = newTransform;
|
||||
AddChild(node3D);
|
||||
beltItem.Disposed += _ => node3D.QueueFree();
|
||||
}
|
||||
// if (node3D.Transform.Origin.DistanceTo(newTransform.Origin) < .00001f)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
_current.Add(new(beltItem, node3D.Transform, newTransform, 0, time));
|
||||
}
|
||||
}
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
_next.Clear();
|
||||
GD.Print(_current.Count);
|
||||
for (int i = 0; i < _current.Count; i++)
|
||||
{
|
||||
var entry = _current[i];
|
||||
if (!IsInstanceValid(_visuals[entry.Item])){
|
||||
_visuals.Remove(entry.Item);
|
||||
continue;
|
||||
}
|
||||
entry.Elapsed = (float)(entry.Elapsed + delta);
|
||||
_visuals[entry.Item].Transform = entry.PreviousTransform.InterpolateWith(entry.NewTransform, entry.Elapsed / entry.Duration);
|
||||
if ((entry.Elapsed / entry.Duration) < 1f)
|
||||
{
|
||||
_next.Add(entry);
|
||||
}
|
||||
}
|
||||
(_current, _next) = (_next, _current);
|
||||
}
|
||||
}
|
||||
public partial class ItemRenderSimple : Node3D, IItemRenderer
|
||||
{
|
||||
private Dictionary<IBeltItem, Node3D> _items = [];
|
||||
|
||||
@@ -25,6 +25,7 @@ public interface IMovementConveyor
|
||||
float SignedSpeed { get; set; }
|
||||
bool IsReversed { get; set; }
|
||||
float Length { get; set; }
|
||||
bool HasItems();
|
||||
// float GetAvailableTravel(ItemConveyor.BeltDirection beltDirection, LaneSpanT itemSpan, float maxDistance);
|
||||
// IBeltSlotProfile GetPortFacingStart();
|
||||
// IBeltSlotProfile GetPortFacingEnd();
|
||||
@@ -41,25 +42,6 @@ public interface IMovementConveyor
|
||||
BeltObstacle GetDistanceToNextItem(BeltDirection beltDirection, float itemBeltT, float maxDistToCheck, LaneSpan laneSpan, HashSet<IMovementConveyor>? visted = null);
|
||||
ConveyorPort CreatePort(BeltPortProfile profile, BeltT beltT, LaneSpan laneSpan);
|
||||
}
|
||||
public readonly struct ConveyorItemHandle
|
||||
{
|
||||
private readonly Action _remove;
|
||||
private readonly Action<ConveyorSlice> _replace;
|
||||
public int Index { get; }
|
||||
public ConveyorSlice Slice { get; }
|
||||
public IBeltItem Item => Slice.Item;
|
||||
public LaneSpan Span => Slice.LaneSpan;
|
||||
public float BeltT => Slice.BeltT;
|
||||
public ConveyorItemHandle(int index, ConveyorSlice slice, Action remove, Action<ConveyorSlice> replace)
|
||||
{
|
||||
Index = index;
|
||||
Slice = slice;
|
||||
_remove = remove;
|
||||
_replace = replace;
|
||||
}
|
||||
public void Remove() => _remove();
|
||||
public void Replace(ConveyorSlice slice) => _replace(slice);
|
||||
}
|
||||
public interface IBeltMovement
|
||||
{
|
||||
void AdvanceBelt(IMovementConveyor conveyor, float delta);
|
||||
@@ -112,7 +94,7 @@ public sealed class IndividualMovement : IBeltMovement
|
||||
}
|
||||
}
|
||||
}
|
||||
enumerator.SortAll();
|
||||
// enumerator.SortAll();
|
||||
}
|
||||
}
|
||||
public sealed class StrictMovement : IBeltMovement
|
||||
@@ -208,9 +190,78 @@ public sealed class StrictMovement : IBeltMovement
|
||||
// }
|
||||
// }
|
||||
|
||||
public void AdvanceBelt(IMovementConveyor conveyor, float delta) => throw new NotImplementedException();
|
||||
}
|
||||
public void AdvanceBelt(IMovementConveyor conveyor, float delta)
|
||||
{
|
||||
|
||||
// GD.Print(conveyor.Items);
|
||||
if (conveyor.GetBeltDirection() == BeltDirection.NotMoving)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!conveyor.HasItems())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// GD.Print("hello");
|
||||
var towardStart = conveyor.GetBeltDirection() == BeltDirection.TowardStart;
|
||||
float maxMove = conveyor.SpeedMagnitude * delta;
|
||||
var enumerator = !towardStart ? conveyor.EnumerateTowardStart() : conveyor.EnumerateTowardEnd();
|
||||
BeltObstacle freeSpace = default!;
|
||||
float lastPos = 0;
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
lastPos = enumerator.Current.Position;
|
||||
freeSpace = conveyor.GetDistanceToNextItem(conveyor.GetBeltDirection(),lastPos,maxMove,LaneSpan.One,[]);
|
||||
}
|
||||
var move = MathF.Min(maxMove, freeSpace.Distance);
|
||||
if (lastPos > conveyor.Length || lastPos < 0)
|
||||
{
|
||||
enumerator = !towardStart ? conveyor.EnumerateTowardStart() : conveyor.EnumerateTowardEnd();
|
||||
while (enumerator.MoveNext() && (enumerator.Current.Position >= conveyor.Length || enumerator.Current.Position <= 0))
|
||||
{
|
||||
if (towardStart ? enumerator.Current.Position <= 0 : enumerator.Current.Position >= conveyor.Length)
|
||||
{
|
||||
var facing = conveyor.GetPortFacing(towardStart ? conveyor.StartPort : conveyor.EndPort);
|
||||
// GD.Print(facing.HasValue(out var slot2),slot2);// , slot2.CanAccept(new TestItem(),LaneSpan.One,0));
|
||||
if (facing.HasValue(out var slot) && slot.TryInsert(enumerator.Current.Value, LaneSpan.One, 0))
|
||||
{
|
||||
// GD.Print(space is PortBeltObstacle port?port.LaneSpan:itemRef.Value.LaneSpan);
|
||||
// GD.PrintS(itemRef.Value.LaneSpan,itemRef.Value.LaneSpan);
|
||||
// GD.Print(itemRef.Item);
|
||||
enumerator.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// var freeSpace = conveyor.GetDistanceToNextItem(conveyor.GetBeltDirection(),conveyor.GetLastItemPosition(),maxMove,LaneSpan.One,[]);
|
||||
if (freeSpace.Distance <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
enumerator = !towardStart ? conveyor.EnumerateTowardStart() : conveyor.EnumerateTowardEnd();
|
||||
if (enumerator.MoveNext())
|
||||
{
|
||||
enumerator.MoveAllBy(MathF.Min(maxMove, freeSpace.Distance));
|
||||
}
|
||||
enumerator = !towardStart ? conveyor.EnumerateTowardStart() : conveyor.EnumerateTowardEnd();
|
||||
while (enumerator.MoveNext() && (enumerator.Current.Position >= conveyor.Length || enumerator.Current.Position <= 0))
|
||||
{
|
||||
if (towardStart ? enumerator.Current.Position <= 0 : enumerator.Current.Position >= conveyor.Length)
|
||||
{
|
||||
var facing = conveyor.GetPortFacing(towardStart ? conveyor.StartPort : conveyor.EndPort);
|
||||
// GD.Print(facing.HasValue(out var slot2),slot2);// , slot2.CanAccept(new TestItem(),LaneSpan.One,0));
|
||||
if (facing.HasValue(out var slot) && slot.TryInsert(enumerator.Current.Value, LaneSpan.One, 0))
|
||||
{
|
||||
// GD.Print(space is PortBeltObstacle port?port.LaneSpan:itemRef.Value.LaneSpan);
|
||||
// GD.PrintS(itemRef.Value.LaneSpan,itemRef.Value.LaneSpan);
|
||||
// GD.Print(itemRef.Item);
|
||||
enumerator.Remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct ConveyorSlice(IBeltItem item, float beltT = 0)
|
||||
@@ -258,7 +309,12 @@ public record BeltTOffset(float T) : BeltT();
|
||||
|
||||
public static class ConveyorExtensions
|
||||
{
|
||||
|
||||
public static BeltDirection Reverse(this BeltDirection beltDirection) => beltDirection switch
|
||||
{
|
||||
BeltDirection.TowardStart => BeltDirection.TowardEnd,
|
||||
BeltDirection.TowardEnd => BeltDirection.TowardStart,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
public static IOption<IBeltPort> GetPortFacing(this IBeltPort slot, IVoxelGridRegistry gridRegistry)
|
||||
{
|
||||
var toCheck = new List<Vector3I>();
|
||||
|
||||
@@ -19,10 +19,12 @@ public partial class TestItemConveyor : Node, IMovementConveyor
|
||||
public override void _Notification(int what) => this.Notify(what);
|
||||
[Dependency] public IBeltMovement MovementSystem => this.DependOn<IBeltMovement>(() => new IndividualMovement());
|
||||
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
|
||||
[Dependency] public IFoodFactoryApi FoodFactoryApi => this.DependOn<IFoodFactoryApi>();
|
||||
// private readonly AutoList<ConveyorSlice> _items = [];
|
||||
// public IAutoList<ConveyorSlice> Items => _items;
|
||||
// public readonly Sorted1DList<ConveyorSlice> Items = new(pos => pos.BeltT);
|
||||
private readonly Ordered1DList<IBeltItem> _items = new();
|
||||
public Ordered1DList<IBeltItem> Items => _items;
|
||||
// private AutoList<ConveyorSlice>.Binding _itemsBinding;
|
||||
public IBeltPort StartPort { get; set; }
|
||||
// public IBeltSlotProfile StartPort => new ItemConveyor.ConveyorPort() {
|
||||
@@ -52,7 +54,7 @@ public partial class TestItemConveyor : Node, IMovementConveyor
|
||||
public IList<IBeltPort> OtherPorts = [];
|
||||
// [Export] public Vector3I Position { get; set; } = default!;
|
||||
//Speed per unit time
|
||||
private readonly AutoValue<float> _speed = new(1);
|
||||
private readonly AutoValue<float> _speed = new(.5f);
|
||||
public IAutoValue<float> SpeedValue => _speed;
|
||||
|
||||
public float SignedSpeed
|
||||
@@ -82,9 +84,9 @@ public partial class TestItemConveyor : Node, IMovementConveyor
|
||||
{
|
||||
return;
|
||||
}
|
||||
var timer = new Timer() { WaitTime = .05f, Autostart = true };
|
||||
AddChild(timer);
|
||||
timer.Timeout += () => MovementSystem.AdvanceBelt(this, (float)timer.WaitTime);
|
||||
// var timer = new Timer() { WaitTime = .05f, Autostart = true };
|
||||
// AddChild(timer);
|
||||
FoodFactoryApi.TickManger.GameTick += args => MovementSystem.AdvanceBelt(this, (float)args.Delta);
|
||||
if (StartPort is null || EndPort is null)
|
||||
{
|
||||
throw new Exception();
|
||||
@@ -127,13 +129,15 @@ public partial class TestItemConveyor : Node, IMovementConveyor
|
||||
}
|
||||
|
||||
// 2️⃣ No local item → try crossing into adjacent conveyor
|
||||
return GetDistanceAcrossPort(
|
||||
var recursiveDistance = GetDistanceAcrossPort(
|
||||
towardStart,
|
||||
itemBeltT,
|
||||
maxDistToCheck,
|
||||
laneSpan,
|
||||
visited
|
||||
);
|
||||
|
||||
return recursiveDistance;// with { Distance = recursiveDistance.Distance + Length };
|
||||
}
|
||||
//ChatGPT Assisted
|
||||
private IOption<ConveyorSlice> FindNextLocalItem(
|
||||
@@ -412,6 +416,7 @@ public partial class TestItemConveyor : Node, IMovementConveyor
|
||||
|
||||
public Ordered1DList<IBeltItem>.Enumerator EnumerateTowardEnd() => _items.EnumerateTowardEnd(-1);
|
||||
public Ordered1DList<IBeltItem>.Enumerator EnumerateTowardStart() => _items.EnumerateTowardStart(_items.Count);
|
||||
public bool HasItems() => _items.Count > 0;
|
||||
}
|
||||
//TODO Should liklely account for max search distance where the conveyorm may be needed to know
|
||||
public record class BeltObstacle(float Distance);
|
||||
|
||||
@@ -18,7 +18,8 @@ public partial class Balancer : Node3D, IProvide<IBeltPortHost>, IProvide<IVoxel
|
||||
private BeltPortHost _insertLogic = default!;
|
||||
public IBeltPortHost Value() => _insertLogic;
|
||||
IVoxelGridRegistry IProvide<IVoxelGridRegistry>.Value() => GridRegistry;
|
||||
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
|
||||
public IVoxelGridRegistry GridRegistry => FoodFactory.GridRegistry;
|
||||
[Dependency] public IFoodFactoryApi FoodFactory => this.DependOn<IFoodFactoryApi>();
|
||||
private List<BeltPort> _outPuts = new();
|
||||
public GridTransform3D VoxelTransform
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ using FoodFactory.Math;
|
||||
using FoodFactory.Voxel;
|
||||
using Godot;
|
||||
|
||||
[Tool]
|
||||
// [Tool]
|
||||
[Meta(typeof(IAutoNode))]
|
||||
public partial class BeltPort : Node3D, IBeltPort
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ public partial class OvenTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVoxel
|
||||
private BeltPortHost _insertLogic = default!;
|
||||
public IBeltPortHost Value() => _insertLogic;
|
||||
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
|
||||
[Dependency] public IFoodFactoryApi FoodFactory => this.DependOn<IFoodFactoryApi>();
|
||||
[Dependency] public IRecipes Recipes => this.DependOn<IRecipes>();
|
||||
IVoxelGridRegistry IProvide<IVoxelGridRegistry>.Value() => GridRegistry;
|
||||
public GridTransform3D VoxelTransform
|
||||
@@ -43,7 +44,7 @@ public partial class OvenTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVoxel
|
||||
{
|
||||
_itemBeingHeld = itemData.GetItem();
|
||||
item.Dispose();
|
||||
GD.Print("Added Item");
|
||||
// GD.Print("Added Item");
|
||||
return true;
|
||||
}
|
||||
throw new NotImplementedException();
|
||||
@@ -51,10 +52,10 @@ public partial class OvenTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVoxel
|
||||
_guid = GridRegistry.Register(this, VoxelTransform.Origin);
|
||||
this.Provide();
|
||||
|
||||
var time = new Timer() { Autostart = true, OneShot = false, WaitTime = .1 };
|
||||
AddChild(time);
|
||||
// var time = new Timer() { Autostart = true, OneShot = false, WaitTime = .1 };
|
||||
// AddChild(time);
|
||||
|
||||
time.Timeout += () =>
|
||||
FoodFactory.TickManger.GameTick += _ =>
|
||||
{
|
||||
if (_itemBeingHeld.Id == -1 && !_itemBeingHeld.IsAlive())//TODO have a better way of dertming if item is valid, possibly nullable
|
||||
{
|
||||
@@ -77,11 +78,12 @@ public partial class OvenTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVoxel
|
||||
unsafe
|
||||
{
|
||||
while (RecipeProcessor.TryProcess(
|
||||
ref recipes,
|
||||
ref context,
|
||||
ref builder,
|
||||
&DestroyEntity,
|
||||
out var created
|
||||
recipes: ref recipes,
|
||||
context: ref context,
|
||||
builder: ref builder,
|
||||
destroyEntity: &DestroyEntity,
|
||||
filter: &RecipeProcessor.FilterByPass,
|
||||
resultEntity: out var created
|
||||
))
|
||||
{
|
||||
Debug.Assert(created.Length <= 1);
|
||||
@@ -178,11 +180,13 @@ public unsafe ref struct RecipeEnumerator : IRecipeEnumerator
|
||||
}
|
||||
public static class RecipeProcessor
|
||||
{
|
||||
public static bool FilterByPass(Recipe recipe) => true;
|
||||
public static unsafe bool TryProcess<TRecipeEnumerator>(
|
||||
ref TRecipeEnumerator recipes,
|
||||
scoped ref RecipeContext context,
|
||||
scoped ref RecipeResultBuilder builder,
|
||||
delegate*<Entity, void> destroyEntity,
|
||||
delegate*<Recipe, bool> filter,
|
||||
out Span<Entity> resultEntity)
|
||||
where TRecipeEnumerator : IRecipeEnumerator, allows ref struct
|
||||
{
|
||||
@@ -198,6 +202,10 @@ public static class RecipeProcessor
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!filter(recipe))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = recipe.Process(context, ref builder);
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ public partial class SlicerTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVox
|
||||
private BeltPortHost _insertLogic = default!;
|
||||
public IBeltPortHost Value() => _insertLogic;
|
||||
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
|
||||
[Dependency] public IFoodFactoryApi FoodFactory => this.DependOn<IFoodFactoryApi>();
|
||||
[Dependency] public IRecipes Recipes => this.DependOn<IRecipes>();
|
||||
IVoxelGridRegistry IProvide<IVoxelGridRegistry>.Value() => GridRegistry;
|
||||
public GridTransform3D VoxelTransform
|
||||
@@ -46,7 +47,7 @@ public partial class SlicerTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVox
|
||||
{
|
||||
_itemBeingHeld = itemData.GetItem();
|
||||
item.Dispose();
|
||||
GD.Print("Added Item to slicer");
|
||||
// GD.Print("Added Item to slicer");
|
||||
return true;
|
||||
}
|
||||
throw new NotImplementedException();
|
||||
@@ -54,10 +55,9 @@ public partial class SlicerTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVox
|
||||
_guid = GridRegistry.Register(this, VoxelTransform.Origin);
|
||||
this.Provide();
|
||||
|
||||
var time = new Timer() { Autostart = true, OneShot = false, WaitTime = .1 };
|
||||
AddChild(time);
|
||||
|
||||
time.Timeout += () =>
|
||||
// var time = new Timer() { Autostart = true, OneShot = false, WaitTime = .1 };
|
||||
// AddChild(time);
|
||||
FoodFactory.TickManger.GameTick += _ =>
|
||||
{
|
||||
|
||||
if (sliced.Any())
|
||||
@@ -83,11 +83,12 @@ public partial class SlicerTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVox
|
||||
unsafe
|
||||
{
|
||||
while (RecipeProcessor.TryProcess(
|
||||
ref recipes,
|
||||
ref context,
|
||||
ref builder,
|
||||
&DestroyEntity,
|
||||
out var created
|
||||
recipes: ref recipes,
|
||||
context: ref context,
|
||||
builder: ref builder,
|
||||
destroyEntity: &DestroyEntity,
|
||||
filter: &RecipeProcessor.FilterByPass,
|
||||
resultEntity: out var created
|
||||
))
|
||||
{
|
||||
_itemBeingHeld = Entity.Null;
|
||||
|
||||
118
src/Equipment/StackerTest.cs
Normal file
118
src/Equipment/StackerTest.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
namespace FoodFactory.Equipment;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Arch.Core;
|
||||
using Arch.Core.Extensions;
|
||||
using Arch.Relationships;
|
||||
using Chickensoft.AutoInject;
|
||||
using Chickensoft.Introspection;
|
||||
using FoodFactory;
|
||||
using FoodFactory.Conveyors;
|
||||
using FoodFactory.Items;
|
||||
using FoodFactory.Math;
|
||||
using FoodFactory.Recipes;
|
||||
using FoodFactory.Voxel;
|
||||
using Godot;
|
||||
using SJK.Functional;
|
||||
|
||||
[Meta(typeof(IAutoNode))]
|
||||
public partial class StackerTest : Node3D, IProvide<IBeltPortHost>, IProvide<IVoxelGridRegistry>
|
||||
{
|
||||
public override void _Notification(int what) => this.Notify(what);
|
||||
private BeltPortHost _insertLogic = default!;
|
||||
public IBeltPortHost Value() => _insertLogic;
|
||||
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
|
||||
[Dependency] public IFoodFactoryApi FoodFactory => this.DependOn<IFoodFactoryApi>();
|
||||
[Dependency] public IRecipes Recipes => this.DependOn<IRecipes>();
|
||||
IVoxelGridRegistry IProvide<IVoxelGridRegistry>.Value() => GridRegistry;
|
||||
public GridTransform3D VoxelTransform
|
||||
{
|
||||
get => GridTransform3D.FromGodot(GlobalTransform);
|
||||
set => GlobalTransform = value.ToGodot();
|
||||
}
|
||||
private Entity _output = Entity.Null;
|
||||
private List<Entity> _inputs = [];
|
||||
private VoxelGuid _guid;
|
||||
|
||||
public void OnResolved()
|
||||
{
|
||||
_insertLogic = new BeltPortHost();
|
||||
_insertLogic.Bind(port => port is BeltPort beltPort && beltPort.PortName == "Input", () => new DelegateInsertBeltItemLogic(canAccept, canInsert));
|
||||
_insertLogic.Bind(port => port is BeltPort beltPort && beltPort.PortName == "OutPut", () => new DelegateInsertBeltItemLogic((_, _) => false, (_, _) => false));
|
||||
bool canAccept(IBeltPort port, IBeltItem item) => _inputs.Count < 2;
|
||||
bool canInsert(IBeltPort beltPort, IBeltItem item)
|
||||
{
|
||||
if (item is IBeltItemData<Entity> itemData)
|
||||
{
|
||||
_inputs.Add(itemData.GetItem());
|
||||
item.Dispose();
|
||||
// GD.Print("Added Item to Stacker");
|
||||
return true;
|
||||
}
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
_guid = GridRegistry.Register(this, VoxelTransform.Origin);
|
||||
this.Provide();
|
||||
|
||||
// var time = new Timer() { Autostart = true, OneShot = false, WaitTime = .1 };
|
||||
// AddChild(time);
|
||||
|
||||
FoodFactory.TickManger.GameTick += _ =>
|
||||
{
|
||||
|
||||
if (_output.Id != -1)
|
||||
{
|
||||
var port = _insertLogic.GetPorts().FirstOrNone(port => port is BeltPort beltPort && beltPort.PortName == "OutPut").Bind(f => f.GetPortFacing(GridRegistry));
|
||||
if (port.HasValue(out var v) && v.TryInsert(new TestItem() { Item = _output }, LaneSpan.One, 0))
|
||||
{
|
||||
_output = Entity.Null;
|
||||
}
|
||||
}
|
||||
if (_inputs.Count < 2)//TODO have a better way of dertming if item is valid, possibly nullable
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<Entity> items = [_inputs[0], _inputs[1]];
|
||||
var context = new RecipeContext(World.Worlds[0], items);
|
||||
var builder = new RecipeResultBuilder(stackalloc bool[10], new ItemBuilder[10]);
|
||||
Span<int> mapping = stackalloc int[2];
|
||||
// var recipe = new PotatoCookRecipe();
|
||||
var recipes = Recipes.GetRecipes("stack", 2, [_inputs[0].Get<Tags>(), _inputs[1].Get<Tags>()], [], new RecipeOutput(1), mapping);
|
||||
unsafe
|
||||
{
|
||||
while (RecipeProcessor.TryProcess(
|
||||
recipes: ref recipes,
|
||||
context: ref context,
|
||||
builder: ref builder,
|
||||
destroyEntity: &DestroyEntity,
|
||||
filter: &RecipeProcessor.FilterByPass,
|
||||
resultEntity: out var created
|
||||
))
|
||||
{
|
||||
_output = created[0];
|
||||
ref var relation = ref _output.GetRelationships<ParentOf>();
|
||||
var count = relation.Count;
|
||||
GD.Print(string.Join(',', relation._elements));
|
||||
foreach (var item in relation)
|
||||
{
|
||||
GD.Print(string.Join(',', item.Key.GetAllComponents()));
|
||||
}
|
||||
_inputs.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
private static void DestroyEntity(Entity entity) => World.Worlds[entity.WorldId].Destroy(entity);
|
||||
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
GridRegistry.UnRegister(_guid);
|
||||
base._ExitTree();
|
||||
}
|
||||
}
|
||||
1
src/Equipment/StackerTest.cs.uid
Normal file
1
src/Equipment/StackerTest.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://culjdbwllmsyk
|
||||
@@ -8,15 +8,15 @@ using System.Linq;
|
||||
|
||||
public readonly struct Tags : IEquatable<Tags>, IReadOnlyCollection<Tag>
|
||||
{
|
||||
private readonly Tag[] _values;
|
||||
private readonly int _index;
|
||||
public Tags()
|
||||
{
|
||||
_values = [];
|
||||
_index = 0;
|
||||
}
|
||||
/// <summary>
|
||||
/// Will error if there is not tags in contained.
|
||||
/// </summary>
|
||||
public readonly Tag First => _values[0];
|
||||
public readonly Tag First => _tags[_index][0];
|
||||
public Tags(params string[] tags)
|
||||
{
|
||||
var arr = new Tag[tags.Length];
|
||||
@@ -25,27 +25,29 @@ public readonly struct Tags : IEquatable<Tags>, IReadOnlyCollection<Tag>
|
||||
arr[i] = TagRegistry.GetTag(tags[i]);
|
||||
}
|
||||
|
||||
_values = SortAndDeduplicate(arr);
|
||||
_index = SortAndDeduplicate(arr);
|
||||
}
|
||||
public override string ToString() => $"Tags: [{string.Join(", ", _values.Select(t => t.Name))}]";
|
||||
private static readonly Dictionary<TagArrayKey, Tag[]> _internedTags = [];
|
||||
public override string ToString() => $"Tags: [{string.Join(", ", _tags[_index].Select(t => t.Name))}]";
|
||||
private static readonly Dictionary<TagArrayKey, int> _internedTags = new(){[new TagArrayKey([])] = 0};
|
||||
private static readonly List<Tag[]> _tags = [[]];
|
||||
|
||||
public readonly int Count => _values.Length;
|
||||
public readonly int Count => _tags[_index].Length;
|
||||
|
||||
private static Tag[] SortAndDeduplicate(Tag[] tags)
|
||||
private static int SortAndDeduplicate(Tag[] tags)
|
||||
{
|
||||
Array.Sort(tags);
|
||||
var key = new TagArrayKey(tags);
|
||||
if (!_internedTags.TryGetValue(key, out var interned))
|
||||
{
|
||||
_internedTags[key] = interned = tags;
|
||||
_tags.Add(tags);
|
||||
_internedTags[key] = _tags.Count - 1;
|
||||
}
|
||||
return interned;
|
||||
}
|
||||
|
||||
public Tags(params Tag[] tags)
|
||||
{
|
||||
_values = SortAndDeduplicate(tags);
|
||||
_index = SortAndDeduplicate(tags);
|
||||
}
|
||||
public Tags(ISet<Tag> tags) : this(tags.ToArray())
|
||||
{
|
||||
@@ -53,30 +55,30 @@ public readonly struct Tags : IEquatable<Tags>, IReadOnlyCollection<Tag>
|
||||
}
|
||||
public Tags With(params Tag[] tags)
|
||||
{
|
||||
HashSet<Tag> set = [.. _values, .. tags];
|
||||
HashSet<Tag> set = [.. _tags[_index], .. tags];
|
||||
return new Tags(set);
|
||||
}
|
||||
public Tags With(params string[] tags)
|
||||
{
|
||||
HashSet<Tag> set = [.. _values, .. TagRegistry.GetTags(tags)];
|
||||
HashSet<Tag> set = [.. _tags[_index], .. TagRegistry.GetTags(tags)];
|
||||
return new Tags(set);
|
||||
}
|
||||
public Tags WithOut(params string[] tags)
|
||||
{
|
||||
HashSet<Tag> set = [.. _values];
|
||||
HashSet<Tag> set = [.. _tags[_index]];
|
||||
for (int i = 0; i < tags.Length; i++)
|
||||
{
|
||||
set.Remove(TagRegistry.GetTag(tags[i]));
|
||||
}
|
||||
return new Tags(set);
|
||||
}
|
||||
public readonly bool Contains(Tag tag) => Array.BinarySearch(_values, tag) >= 0;
|
||||
public readonly bool Equals(Tags other) => _values == other._values;
|
||||
public readonly bool Contains(Tag tag) => Array.BinarySearch(_tags[_index], tag) >= 0;
|
||||
public readonly bool Equals(Tags other) => _index == other._index;
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj) => obj is Tags tags && Equals(tags);
|
||||
public override readonly int GetHashCode() => _values.GetHashCode();
|
||||
public override readonly int GetHashCode() => _tags[_index].GetHashCode();
|
||||
public readonly IEnumerator<Tag> GetEnumerator()
|
||||
{
|
||||
foreach (var item in _values)
|
||||
foreach (var item in _tags[_index])
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
@@ -140,32 +142,40 @@ public readonly struct Tags : IEquatable<Tags>, IReadOnlyCollection<Tag>
|
||||
}
|
||||
public readonly bool ContainsAll(Tags other)
|
||||
{
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
|
||||
while (i < _values.Length && j < other._values.Length)
|
||||
foreach (var item in other)
|
||||
{
|
||||
if (_values[i] == other._values[j])
|
||||
{
|
||||
i++;
|
||||
j++;
|
||||
}
|
||||
else if (_values[i] < other._values[j])
|
||||
if (!Contains(item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
j++;
|
||||
}
|
||||
}
|
||||
return j == other._values.Length;
|
||||
return true;
|
||||
// var i = 0;
|
||||
// var j = 0;
|
||||
|
||||
// while (i < _tags[_index].Length && j < _tags[other._index].Length)
|
||||
// {
|
||||
// if (_tags[_index][i] == _tags[other._index][j])
|
||||
// {
|
||||
// i++;
|
||||
// j++;
|
||||
// }
|
||||
// else if (_tags[_index][i] < _tags[other._index][j])
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// j++;
|
||||
// }
|
||||
// }
|
||||
// return j == _tags[other._index].Length;
|
||||
}
|
||||
public readonly bool ContainsAny(Tags other)
|
||||
{
|
||||
for (var i = 0; i < _values.Length; i++)
|
||||
for (var i = 0; i < _tags[_index].Length; i++)
|
||||
{
|
||||
if (other.Contains(_values[i]))
|
||||
if (other.Contains(_tags[_index][i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ public class TestItem() : IBeltItem, IBeltItemData<Entity>
|
||||
}
|
||||
else
|
||||
{
|
||||
node = new MeshInstance3D() { Mesh = new BoxMesh() };
|
||||
node = new MeshInstance3D() { Mesh = new BoxMesh() { Size = new Vector3(.2f, .2f, .2f) } };
|
||||
}
|
||||
node.GetChildren().OfType<Node3D>().ToList().ForEach(i => i.Scale *= new Vector3(.2f, .2f, .2f));
|
||||
if (Item.TryGet<Color>(out var color))
|
||||
@@ -81,23 +81,12 @@ public class TestItem() : IBeltItem, IBeltItemData<Entity>
|
||||
|
||||
public Entity Item;
|
||||
}
|
||||
|
||||
public interface IFluidItem
|
||||
{
|
||||
float Volume { get; }
|
||||
}
|
||||
public interface IConveyorProfile
|
||||
{
|
||||
int LaneCount { get; }
|
||||
bool LaneSwitching { get; }
|
||||
|
||||
}
|
||||
public readonly record struct BeltPortProfile(
|
||||
GridTransform3D LocalOffset,
|
||||
// Direction Face,
|
||||
int Width,
|
||||
PortAccess Access
|
||||
) : IBeltSlotProfile
|
||||
)
|
||||
{
|
||||
public Vector3I Position => LocalOffset.Origin;
|
||||
// Direction IBeltSlotProfile.Direction => Face;
|
||||
@@ -186,15 +175,6 @@ public sealed class ConveyorPort : IBeltPort
|
||||
return _accept!(item, beltT, laneSpan);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface IBeltSlotProfile
|
||||
{
|
||||
Vector3I Position { get; }
|
||||
int Width { get; }
|
||||
PortAccess Access { get; }
|
||||
}
|
||||
// public record LaneId(int Index);
|
||||
[Flags]
|
||||
public enum PortAccess : byte
|
||||
{
|
||||
@@ -204,14 +184,6 @@ public enum PortAccess : byte
|
||||
InOut = In | Out,
|
||||
BiDirectional = InOut
|
||||
}
|
||||
[Flags]
|
||||
public enum TransferMode : byte//Need Better Name
|
||||
{
|
||||
Passive = 0,
|
||||
Push = 1,
|
||||
Pull = 2,
|
||||
PushPull = Push | Pull
|
||||
}
|
||||
public static class SlotExtension
|
||||
{
|
||||
public static LaneSpan MapLaneSpanToFacingPort(this IBeltPort self, IBeltPort other) => MapSlotToFacingSlot(self.Profile.LocalOffset, self.Profile.Width, other.Profile.LocalOffset, other.Profile.Width);
|
||||
|
||||
@@ -3,10 +3,13 @@ namespace FoodFactory;
|
||||
using System.Collections.Generic;
|
||||
using Arch.Core;
|
||||
using Arch.Core.Extensions;
|
||||
using Arch.Core.Utils;
|
||||
using Arch.LowLevel;
|
||||
using Arch.Persistence;
|
||||
using FoodFactory.Items;
|
||||
using FoodFactory.Math;
|
||||
using Godot;
|
||||
using Utf8Json;
|
||||
|
||||
public class Test
|
||||
{
|
||||
@@ -51,22 +54,63 @@ public class Test
|
||||
GD.Print(type);
|
||||
|
||||
var flour = world.Create(new Name("flour"), new Temperature(71, TemperatureUnit.Fahrenheit), new Tags("flour", "wheat"));
|
||||
var tag2 = new Tags();
|
||||
// var tag2 = new Tags();
|
||||
var serializer = new ArchJsonSerializer(new TagsSerializer(), new TemperatureSerializer());
|
||||
world.TrimExcess();
|
||||
var worldJson = serializer.ToJson(world);
|
||||
// GD.Print(worldJson);
|
||||
var otherWorld = serializer.FromJson(worldJson);
|
||||
|
||||
|
||||
world.SubscribeEntityDestroyed((in entity) =>
|
||||
{
|
||||
foreach (var item in entity.GetAllComponents())
|
||||
{
|
||||
if (item is IEntityContainer container)
|
||||
{
|
||||
foreach (var item2 in container.GetEntities())
|
||||
{
|
||||
world.Destroy(item2);
|
||||
}
|
||||
}
|
||||
public class TagsSerializer : IJsonFormatter<Tags>
|
||||
{
|
||||
public Tags Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
List<string> names = [];
|
||||
reader.ReadIsBeginObject();
|
||||
reader.ReadIsBeginArray();
|
||||
while (!reader.ReadIsEndArray())
|
||||
{
|
||||
names.Add(reader.ReadString());
|
||||
}
|
||||
reader.ReadIsEndObject();
|
||||
return new Tags([.. names]);
|
||||
}
|
||||
|
||||
public void Serialize(ref JsonWriter writer, Tags value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
writer.WriteBeginArray();
|
||||
if (value.Count > 0)
|
||||
{
|
||||
foreach (var item in value)
|
||||
{
|
||||
writer.WriteString(item.Name);
|
||||
}
|
||||
}
|
||||
);
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
public class TemperatureSerializer : IJsonFormatter<Temperature>
|
||||
{
|
||||
public Temperature Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
reader.ReadIsBeginObject();
|
||||
reader.ReadPropertyName();
|
||||
var temp = reader.ReadDouble();
|
||||
reader.ReadIsEndObject();
|
||||
return new(temp);
|
||||
}
|
||||
|
||||
public void Serialize(ref JsonWriter writer, Temperature value, IJsonFormatterResolver formatterResolver)
|
||||
{
|
||||
writer.WriteBeginObject();
|
||||
writer.WritePropertyName(nameof(value.Kelvin));
|
||||
writer.WriteDouble(value.Kelvin);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +123,7 @@ public record struct Name(string Value);
|
||||
public record struct BurnableTemp(Temperature Temperature);
|
||||
public record struct BurnedItem(Temperature BurnedAt, string Description);//Info about how burnted item is
|
||||
public record struct MarcoNutrients(Weight Fat, Weight Protein, Carbohydrates Carbohydrate);
|
||||
public record struct ParentOf();
|
||||
public readonly record struct Carbohydrates(
|
||||
Weight Fiber,
|
||||
Weight Starch,
|
||||
|
||||
@@ -2,13 +2,15 @@ namespace SJK.Math;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Chickensoft.Sync.Primitives;
|
||||
|
||||
public sealed class Ordered1DList<T>// : IDisposable where T : unmanaged
|
||||
{
|
||||
private readonly List<Entry> _values;
|
||||
public Ordered1DList(int capacity = 8)
|
||||
private readonly AutoList<Entry> _values;
|
||||
public IAutoList<Entry> Values => _values;
|
||||
public Ordered1DList()
|
||||
{
|
||||
_values = new(capacity);
|
||||
_values = [];
|
||||
}
|
||||
public record struct Entry(float Position, T Value) : IComparable<Entry>
|
||||
{
|
||||
@@ -109,17 +111,19 @@ public sealed class Ordered1DList<T>// : IDisposable where T : unmanaged
|
||||
_list._values.RemoveAt(_index);
|
||||
_index -= _towardsEnd ? 1 : -1;
|
||||
}
|
||||
public void SortAll()
|
||||
{
|
||||
if (_list.Count > 0)
|
||||
{
|
||||
_list._values.Sort();
|
||||
}
|
||||
}
|
||||
public readonly void MoveAllBy(float offset) => _list.MoveAllBy(offset * (_towardsEnd ?-1:1));
|
||||
// public readonly void SortAll()
|
||||
// {
|
||||
// if (_list.Count > 0)
|
||||
// {
|
||||
// _list._values.Sort();
|
||||
// }
|
||||
// }
|
||||
|
||||
public readonly void Set(float newPosition)
|
||||
public readonly void Set(float newPosition, bool sortLocal = true)
|
||||
{
|
||||
_list._values[_index] = new(newPosition, _list._values[_index].Value);
|
||||
_list.SortFromIndex(_index);
|
||||
// _list.SortFromIndex(_index);
|
||||
// _list.SortFromIndex(_index);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using Arch.Core;
|
||||
using Arch.Core.Extensions;
|
||||
using Arch.Core.Extensions.Dangerous;
|
||||
using Arch.Relationships;
|
||||
using FoodFactory.Items;
|
||||
using FoodFactory.Math;
|
||||
using Godot;
|
||||
@@ -66,7 +67,7 @@ public static class RecipeCompiler
|
||||
{
|
||||
RecipeRef = recipe,
|
||||
ItemEntries = BuildEntries(recipe),
|
||||
// IsOrdered = recipe.InputsOrdered,
|
||||
IsOrdered = recipe.InputsOrdered,
|
||||
};
|
||||
private static ItemEntry[] BuildEntries(Recipe recipe)
|
||||
{
|
||||
@@ -147,8 +148,9 @@ public record OnionSliceRecipe() : Recipe(Name: "Onion_Slice",
|
||||
private Entity Slice(in RecipeContext context)
|
||||
{
|
||||
Span<Entity> span = stackalloc Entity[1];
|
||||
var signature = context.World.GetSignature(context.Items[0]);
|
||||
context.World.Create(span, signature, 1);
|
||||
span[0] = context.World.Create(context.Items[0].GetComponentTypes());
|
||||
// var signature = context.World.GetSignature(context.Items[0]);
|
||||
// context.World.Create(span, signature, 1);
|
||||
PotatoCookRecipe.CopyEntities(context.Items, span, context.World);
|
||||
span[0].Set(new Name("Sliced " + context.Items[0].Get<Name>().Value));
|
||||
span[0].Set(new Mass(context.Items[0].Get<Mass>().Value * .5));
|
||||
@@ -156,6 +158,32 @@ public record OnionSliceRecipe() : Recipe(Name: "Onion_Slice",
|
||||
return span[0];
|
||||
}
|
||||
}
|
||||
public record StackRecipe() : Recipe(Name: "Stack_Recipe",
|
||||
BlueprintIds: null,
|
||||
RequireAll: [false, false],
|
||||
Inputs: [new Tags("raw"), new Tags()],
|
||||
Exclude: [new Tags("raw"), new Tags()],
|
||||
Action: "stack",
|
||||
RecipeOutput: new RecipeOutput(1)
|
||||
)
|
||||
{
|
||||
public override bool CanProcess(in RecipeContext context) => true;
|
||||
|
||||
public override RecipeResult Process(in RecipeContext context, ref RecipeResultBuilder builder)
|
||||
{
|
||||
builder.AddRemove(false);
|
||||
builder.AddRemove(false);
|
||||
builder.AddCreate(Stack);
|
||||
return builder.Build();
|
||||
}
|
||||
private Entity Stack(in RecipeContext context)
|
||||
{
|
||||
var newItem = context.World.Create(new Name("Stack of " + context.Items[0].Get<Name>().Value + " and " + context.Items[1].Get<Name>().Value), new Tags("stack"));
|
||||
newItem.AddRelationship<ParentOf>(context.Items[0]);
|
||||
newItem.AddRelationship<ParentOf>(context.Items[1]);
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
public record PotatoCookRecipe() : Recipe(Name: "Potato_Cook",
|
||||
BlueprintIds: null,
|
||||
RequireAll: [false],
|
||||
@@ -183,8 +211,9 @@ public record PotatoCookRecipe() : Recipe(Name: "Potato_Cook",
|
||||
private Entity NewPotato(in RecipeContext context)
|
||||
{
|
||||
Span<Entity> span = stackalloc Entity[1];
|
||||
var signature = context.World.GetSignature(context.Items[0]);
|
||||
context.World.Create(span, signature, 1);
|
||||
span[0] = context.World.Create(context.Items[0].GetComponentTypes());
|
||||
// var signature = context.World.GetSignature(context.Items[0]);
|
||||
// context.World.Create(span, signature, 1);
|
||||
CopyEntity(context.Items[0], span[0], context.World);
|
||||
|
||||
span[0].Set(new Name("Cooked_Potato"));
|
||||
|
||||
@@ -28,6 +28,7 @@ public class Recipes : IRecipes
|
||||
builder.Add(new PotatoCookRecipe());
|
||||
builder.Add(new BurnItemRecipe());
|
||||
builder.Add(new OnionSliceRecipe());
|
||||
builder.Add(new StackRecipe());
|
||||
_sortedRecipes = builder.Build();
|
||||
_recipes = builder.CompiledRecipes.Select(f => f.RecipeRef).ToDictionary(k => new RecipeName(k.Name));
|
||||
}
|
||||
@@ -80,7 +81,8 @@ public class Recipes : IRecipes
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (tags.ContainsAny(compiled.ExcludeTags))
|
||||
|
||||
if (compiled.ExcludeTags.Count != 0 && tags.ContainsAny(compiled.ExcludeTags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
49365
src/VoxelGrid/Stress.tscn
Normal file
49365
src/VoxelGrid/Stress.tscn
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,10 +43,12 @@ public class BacterialSystem : BaseSystem<World, float>
|
||||
|
||||
//Will Liklely be the game instead of a node like this
|
||||
[Meta(typeof(IAutoNode))]// [Tool]
|
||||
public partial class VoxelGridNode : Node3D, IProvide<IVoxelGridRegistry>, IProvide<IItemRenderer>, IProvide<IRecipes>, IProvide<IBlueprintManger>, IProvide<World>
|
||||
public partial class VoxelGridNode : Node3D, IProvide<IVoxelGridRegistry>, IProvide<IItemRenderer>, IProvide<IRecipes>, IProvide<IBlueprintManger>, IProvide<World>, IProvide<IFoodFactoryApi>
|
||||
{
|
||||
public override void _Notification(int what) => this.Notify(what);
|
||||
|
||||
private IFoodFactoryApi _api = default!;
|
||||
IFoodFactoryApi IProvide<IFoodFactoryApi>.Value() => _api;
|
||||
private World _world = default!;
|
||||
World IProvide<World>.Value() => _world;
|
||||
private IVoxelGridRegistry _voxelGridRegistry = default!;
|
||||
@@ -63,24 +65,40 @@ public partial class VoxelGridNode : Node3D, IProvide<IVoxelGridRegistry>, IProv
|
||||
// GD.Print();
|
||||
base._Ready();
|
||||
_voxelGridRegistry = new VoxelRegistry();
|
||||
_itemRenderer = new ItemRenderSimple();
|
||||
_itemRenderer = new ItemRenderBuffered();
|
||||
_blueprintManger = new BlueprintManger();
|
||||
_recipes = new Recipes();
|
||||
_world = World.Create();
|
||||
_systems = new Group<float>("Items", new BacterialSystem(_world));
|
||||
var tickManger = new TickManger();
|
||||
|
||||
|
||||
AddChild(_itemRenderer as Node);
|
||||
Timer timer = new Timer() { WaitTime = .25f, Autostart = true };//TEST
|
||||
var delta = .2f;
|
||||
Timer timer = new Timer() { WaitTime = delta, Autostart = true };//TEST
|
||||
AddChild(timer);//TEST
|
||||
// timer.Timeout += _itemRenderer.Tick;//TEST
|
||||
_api = new FoodFactoryApi()
|
||||
{
|
||||
BlueprintManger = _blueprintManger,
|
||||
Recipes = _recipes,
|
||||
ItemRenderer = _itemRenderer,
|
||||
TickManger = tickManger,
|
||||
GridRegistry = _voxelGridRegistry
|
||||
};
|
||||
int tick = 0;
|
||||
timer.Timeout += () =>
|
||||
{
|
||||
_systems.BeforeUpdate(.25f);
|
||||
_systems.Update(.25f);
|
||||
_systems.AfterUpdate(.25f);
|
||||
_systems.BeforeUpdate(delta);
|
||||
_systems.Update(delta);
|
||||
_systems.AfterUpdate(delta);
|
||||
tickManger.BroadCast(new(tick, delta));
|
||||
tick++;
|
||||
|
||||
};
|
||||
_systems.Initialize();
|
||||
|
||||
|
||||
this.Provide();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
[gd_scene format=3 uid="uid://dfacxkkkc0v10"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dcrb286hmpli" path="res://src/VoxelGrid/VoxelGridNode.cs" id="1_tsdpe"]
|
||||
[ext_resource type="Script" uid="uid://cnkblltup5guy" path="res://src/VoxelGrid/OvenTest.cs" id="3_r7dgx"]
|
||||
[ext_resource type="Script" uid="uid://cnkblltup5guy" path="res://src/Equipment/OvenTest.cs" id="3_r7dgx"]
|
||||
[ext_resource type="PackedScene" uid="uid://bktqs1lw6go4" path="res://src/VoxelGrid/ItemSpawner.tscn" id="5_mxaon"]
|
||||
[ext_resource type="PackedScene" uid="uid://h00mq2srsbfa" path="res://assets/kenney_conveyor-kit/Models/GLB format/door.glb" id="5_wk2t5"]
|
||||
[ext_resource type="Script" uid="uid://ee5aoxi8mjnw" path="res://src/VoxelGrid/BeltPort.cs" id="6_2wkfx"]
|
||||
[ext_resource type="Script" uid="uid://ee5aoxi8mjnw" path="res://src/Equipment/BeltPort.cs" id="6_2wkfx"]
|
||||
[ext_resource type="PackedScene" uid="uid://c4h7mwnfrdesg" path="res://src/Conveyors/ConveyorBeltStraight/ConveyorBeltStraight.tscn" id="6_mxaon"]
|
||||
[ext_resource type="Script" uid="uid://opbkqoaa7x2n" path="res://src/VoxelGrid/Balancer.cs" id="7_2wkfx"]
|
||||
[ext_resource type="Script" uid="uid://yec84plemjv1" path="res://src/VoxelGrid/SlicerTest.cs" id="8_2lg7i"]
|
||||
[ext_resource type="Script" uid="uid://opbkqoaa7x2n" path="res://src/Equipment/Balancer.cs" id="7_2wkfx"]
|
||||
[ext_resource type="Script" uid="uid://yec84plemjv1" path="res://src/Equipment/SlicerTest.cs" id="8_2lg7i"]
|
||||
[ext_resource type="Script" uid="uid://culjdbwllmsyk" path="res://src/Equipment/StackerTest.cs" id="8_e2skk"]
|
||||
|
||||
[sub_resource type="Curve3D" id="Curve3D_mxaon"]
|
||||
_data = {
|
||||
@@ -64,6 +65,12 @@ transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 5, 0
|
||||
[node name="ConveyorBeltStraight27" parent="." unique_id=1546024538 instance=ExtResource("6_mxaon")]
|
||||
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 5, 0, 6)
|
||||
|
||||
[node name="ConveyorBeltStraight42" parent="." unique_id=2038052188 instance=ExtResource("6_mxaon")]
|
||||
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 4, 0, 6)
|
||||
|
||||
[node name="ConveyorBeltStraight43" parent="." unique_id=131250040 instance=ExtResource("6_mxaon")]
|
||||
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 3, 0, 6)
|
||||
|
||||
[node name="ConveyorBeltStraight28" parent="." unique_id=1945842047 instance=ExtResource("6_mxaon")]
|
||||
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 5, 0, 4)
|
||||
|
||||
@@ -189,14 +196,14 @@ transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 1
|
||||
Access = 2
|
||||
|
||||
[node name="Node3D3" type="Node3D" parent="Balancer" unique_id=1846433120]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 2
|
||||
Access = 1
|
||||
|
||||
[node name="Node3D4" type="Node3D" parent="Balancer" unique_id=1579207766]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 1)
|
||||
@@ -216,6 +223,66 @@ Access = 2
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0.5)
|
||||
mesh = SubResource("BoxMesh_2wkfx")
|
||||
|
||||
[node name="Balancer3" type="Node3D" parent="." unique_id=457806568]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, 2)
|
||||
script = ExtResource("7_2wkfx")
|
||||
|
||||
[node name="Node3D" type="Node3D" parent="Balancer3" unique_id=1019210964]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 2
|
||||
|
||||
[node name="Node3D3" type="Node3D" parent="Balancer3" unique_id=1520385636]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 1
|
||||
|
||||
[node name="Node3D4" type="Node3D" parent="Balancer3" unique_id=2034092939]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 1)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 1
|
||||
|
||||
[node name="Node3D2" type="Node3D" parent="Balancer3" unique_id=1013256336]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0, 1)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 2
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Balancer3" unique_id=920267076]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0.5)
|
||||
mesh = SubResource("BoxMesh_2wkfx")
|
||||
|
||||
[node name="Balancer2" type="Node3D" parent="." unique_id=620786657]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, 6)
|
||||
script = ExtResource("8_e2skk")
|
||||
|
||||
[node name="Node3D" type="Node3D" parent="Balancer2" unique_id=1570745328]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 3
|
||||
PortName = "OutPut"
|
||||
|
||||
[node name="Node3D3" type="Node3D" parent="Balancer2" unique_id=1793642533]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 3
|
||||
PortName = "Input"
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Balancer2" unique_id=1733942784]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0.5)
|
||||
mesh = SubResource("BoxMesh_2wkfx")
|
||||
|
||||
[node name="Balancer8" type="Node3D" parent="." unique_id=1783081047]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 7, 0, -1)
|
||||
script = ExtResource("7_2wkfx")
|
||||
@@ -225,14 +292,14 @@ transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 1
|
||||
Access = 2
|
||||
|
||||
[node name="Node3D3" type="Node3D" parent="Balancer8" unique_id=51764402]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 2
|
||||
Access = 1
|
||||
|
||||
[node name="Node3D4" type="Node3D" parent="Balancer8" unique_id=233486636]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, 0, 0, 1)
|
||||
@@ -258,6 +325,11 @@ script = ExtResource("8_2lg7i")
|
||||
|
||||
[node name="Node3D4" type="Node3D" parent="Slicer" unique_id=1206144064]
|
||||
transform = Transform3D(-1, 0, 8.742277e-08, 0, 1, 0, -8.742277e-08, 0, -1, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 1
|
||||
PortName = "Input"
|
||||
|
||||
[node name="Path3D" type="Path3D" parent="Slicer/Node3D4" unique_id=894348025]
|
||||
curve = SubResource("Curve3D_mxaon")
|
||||
@@ -267,6 +339,11 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.870017e-08, 0, -0.6001501)
|
||||
|
||||
[node name="Node3D5" type="Node3D" parent="Slicer" unique_id=1113082191]
|
||||
transform = Transform3D(1, 0, -1.7484555e-07, 0, 1, 0, 1.7484555e-07, 0, 1, 0, 0, 0)
|
||||
script = ExtResource("6_2wkfx")
|
||||
Face = 4
|
||||
Width = 1
|
||||
Access = 2
|
||||
PortName = "OutPut"
|
||||
|
||||
[node name="Path3D2" type="Path3D" parent="Slicer/Node3D5" unique_id=1352328131]
|
||||
curve = SubResource("Curve3D_mxaon")
|
||||
|
||||
Reference in New Issue
Block a user