feat: initial commit

This commit is contained in:
Dylan R. E. Moonfire 2021-09-06 23:15:21 -05:00
commit e9dad5bbe4
29 changed files with 15100 additions and 0 deletions

122
.editorconfig Normal file
View file

@ -0,0 +1,122 @@
# EditorConfig is awesome: https://EditorConfig.org
root = true
[*]
charset=utf-8
end_of_line = lf
insert_final_newline=true
indent_style=space
indent_size=4
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers=false
csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_space_after_cast=false
csharp_style_var_elsewhere=false:hint
csharp_style_var_for_built_in_types=false:hint
csharp_style_var_when_type_is_apparent=true:hint
csharp_preserve_single_line_statements=false
csharp_preserve_single_line_blocks=true
dotnet_style_predefined_type_for_locals_parameters_members=true:hint
dotnet_style_predefined_type_for_member_access=true:hint
dotnet_style_qualification_for_event=true:hint
dotnet_style_qualification_for_field=true:hint
dotnet_style_qualification_for_method=true:hint
dotnet_style_qualification_for_property=true:hint
dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint
# ReSharper properties
resharper_alignment_tab_fill_style=optimal_fill
resharper_apply_on_completion=true
resharper_blank_lines_after_control_transfer_statements=1
resharper_blank_lines_around_single_line_auto_property=1
resharper_blank_lines_around_single_line_property=1
resharper_blank_lines_before_single_line_comment=1
resharper_blank_lines_between_using_groups=1
resharper_braces_for_for=required
resharper_braces_for_foreach=required
resharper_braces_for_ifelse=required
resharper_braces_for_while=required
resharper_can_use_global_alias=false
resharper_csharp_blank_lines_around_single_line_field=1
resharper_csharp_blank_lines_around_single_line_invocable=1
resharper_csharp_indent_style=tab
resharper_csharp_insert_final_newline=true
resharper_csharp_keep_blank_lines_in_code=1
resharper_csharp_keep_blank_lines_in_declarations=1
resharper_csharp_new_line_before_while=true
resharper_csharp_use_indent_from_vs=false
resharper_csharp_wrap_arguments_style=chop_if_long
resharper_csharp_wrap_extends_list_style=chop_if_long
resharper_csharp_wrap_parameters_style=chop_if_long
resharper_css_insert_final_newline=false
resharper_enforce_line_ending_style=true
resharper_html_insert_final_newline=false
resharper_indent_nested_fixed_stmt=true
resharper_js_indent_style=tab
resharper_js_insert_final_newline=true
resharper_js_keep_blank_lines_in_code=1
resharper_js_stick_comment=false
resharper_js_use_indent_from_vs=false
resharper_js_wrap_before_binary_opsign=true
resharper_js_wrap_chained_method_calls=chop_if_long
resharper_keep_blank_lines_between_declarations=1
resharper_min_blank_lines_after_imports=1
resharper_place_attribute_on_same_line=False
resharper_place_constructor_initializer_on_same_line=false
resharper_place_type_constraints_on_same_line=false
resharper_protobuf_insert_final_newline=false
resharper_qualified_using_at_nested_scope=true
resharper_resx_insert_final_newline=false
resharper_space_within_single_line_array_initializer_braces=true
resharper_use_indents_from_main_language_in_file=false
resharper_vb_insert_final_newline=false
resharper_wrap_after_declaration_lpar=true
resharper_wrap_after_invocation_lpar=true
resharper_wrap_before_extends_colon=true
resharper_wrap_before_first_type_parameter_constraint=true
resharper_wrap_before_type_parameter_langle=true
resharper_xmldoc_indent_child_elements=ZeroIndent
resharper_xmldoc_indent_text=ZeroIndent
resharper_xmldoc_insert_final_newline=false
resharper_xml_insert_final_newline=false
# ReSharper inspection severities
resharper_check_namespace_highlighting=none
resharper_convert_to_auto_property_highlighting=none
resharper_localizable_element_highlighting=none
resharper_redundant_comma_in_attribute_list_highlighting=none
resharper_redundant_comma_in_enum_declaration_highlighting=none
resharper_redundant_comma_in_initializer_highlighting=none
resharper_string_compare_to_is_culture_specific_highlighting=none
resharper_string_index_of_is_culture_specific_1_highlighting=none
resharper_use_null_propagation_highlighting=none
resharper_use_object_or_collection_initializer_highlighting=hint
resharper_use_string_interpolation_highlighting=hint
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style=space
indent_size=2
tab_width=2
[*.{cs,js,json,jsx,proto,resjson,ts,tsx}]
indent_style=space
indent_size=space
tab_width=4
[*.{asax,ascx,aspx,cshtml,css,htm,html,master,razor,skin,vb,xaml,xamlx,xoml}]
indent_style=space
indent_size=4
tab_width=4
[*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,jsproj,lsproj,njsproj,nuspec,proj,props,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}]
indent_style=space
indent_size=2
tab_width=2
[*.proto]
indent_style=space
indent_size=2
tab_width=2

1
.envrc Normal file
View file

@ -0,0 +1 @@
use asdf

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
launchSettings.json
*~
*.user
Directory.Build.props
obj/
[Bb]in/
.vs/
.vscode/
.idea/
_ReSharper.Caches/
node_modules/

51
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,51 @@
stages:
- build
default:
before_script:
- curl -sL https://deb.nodesource.com/setup_15.x | bash -
- apt-get install -y nodejs
build:
image: mcr.microsoft.com/dotnet/sdk:5.0
stage: build
script:
# Set up the environment.
- npx npm install --ci
- npx commitlint-gitlab-ci -x @commitlint/config-conventional
# Build and test everything.
- dotnet restore
- dotnet build
- 'dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=../artifacts/{assembly}-test-result.xml;MethodFormat=Default;FailureBodyFormat=Verbose" --collect:"XPlat Code Coverage"'
# Summarize the output for Gitlab CI reporting.
- dotnet new tool-manifest
- dotnet tool install dotnet-reportgenerator-globaltool
- dotnet tool run reportgenerator -reports:src/*/TestResults/*/coverage.cobertura.xml -targetdir:./coverage "-reporttypes:Cobertura;TextSummary"
- grep "Line coverage" coverage/Summary.txt
# Perform the release.
- npx semantic-release
rules:
- if: '$CI_COMMIT_TITLE =~ /^chore\(release\)/'
when: never
- if: '$CI_COMMIT_TAG'
when: never
- if: '$CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH'
when: never
- when: on_success
artifacts:
when: always
paths:
- ./**/*test-result.xml
- ./coverage/Cobertura.xml
- ./coverage/Summary.*
- ./**/*.nupkg
reports:
junit:
- ./**/*test-result.xml
cobertura:
- ./coverage/Cobertura.xml

4
.husky/commit-msg Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit $1

3
.tool-versions Normal file
View file

@ -0,0 +1,3 @@
dotnet-core 5.0.100
yarn 1.22.10
nodejs 15.0.1

29
CHANGELOG.md Normal file
View file

@ -0,0 +1,29 @@
## [1.1.4](https://gitlab.com/mfgames-cil/mfgames-locking-cil/compare/v1.1.3...v1.1.4) (2021-09-04)
### Bug Fixes
* **semantic-release-nuget:** fixing typo ([4ce4fe2](https://gitlab.com/mfgames-cil/mfgames-locking-cil/commit/4ce4fe26cb3d8e90a7ef710a82626af8e7caa147))
## [1.1.3](https://gitlab.com/mfgames-cil/mfgames-locking-cil/compare/v1.1.2...v1.1.3) (2021-09-04)
### Bug Fixes
* working on pushing ([5c66619](https://gitlab.com/mfgames-cil/mfgames-locking-cil/commit/5c66619e12587ada01c5eafd5a4aa5d8f6ef3c5d))
## [1.1.2](https://gitlab.com/mfgames-cil/mfgames-locking-cil/compare/v1.1.1...v1.1.2) (2021-09-04)
### Bug Fixes
* updating package-lock.json ([d000882](https://gitlab.com/mfgames-cil/mfgames-locking-cil/commit/d000882f96895ffa42c000f0169048354db78bc1))
* working on build patterns ([7594b73](https://gitlab.com/mfgames-cil/mfgames-locking-cil/commit/7594b73846b80127ee6ba7ab1d3d0f6d64c81cc5))
* working on dependencies ([ba538c0](https://gitlab.com/mfgames-cil/mfgames-locking-cil/commit/ba538c0d86005618c84f3fc6bbf062d60e499a1b))
## [1.1.1](https://gitlab.com/mfgames-cil/mfgames-locking-cil/compare/v1.1.0...v1.1.1) (2021-01-22)
### Bug Fixes
* added changelog generation ([d8fa302](https://gitlab.com/mfgames-cil/mfgames-locking-cil/commit/d8fa3029a9c2d0c9523584a78e16d4612fd114f8))

54
Gallium.sln Normal file
View file

@ -0,0 +1,54 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4C5CBAB6-6400-4C7C-B192-B28F07DD722B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallium", "src\Gallium\Gallium.csproj", "{940230CF-253F-4DA2-95FA-9DFB77089D4F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gallium.Tests", "src\Gallium.Tests\Gallium.Tests.csproj", "{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Debug|x64.ActiveCfg = Debug|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Debug|x64.Build.0 = Debug|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Debug|x86.ActiveCfg = Debug|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Debug|x86.Build.0 = Debug|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Release|Any CPU.Build.0 = Release|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Release|x64.ActiveCfg = Release|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Release|x64.Build.0 = Release|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Release|x86.ActiveCfg = Release|Any CPU
{940230CF-253F-4DA2-95FA-9DFB77089D4F}.Release|x86.Build.0 = Release|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Debug|x64.ActiveCfg = Debug|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Debug|x64.Build.0 = Debug|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Debug|x86.ActiveCfg = Debug|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Debug|x86.Build.0 = Debug|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Release|Any CPU.Build.0 = Release|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Release|x64.ActiveCfg = Release|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Release|x64.Build.0 = Release|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Release|x86.ActiveCfg = Release|Any CPU
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{940230CF-253F-4DA2-95FA-9DFB77089D4F} = {4C5CBAB6-6400-4C7C-B192-B28F07DD722B}
{D6E5D0F6-8CDB-40B5-96D4-8719FC724AE5} = {4C5CBAB6-6400-4C7C-B192-B28F07DD722B}
EndGlobalSection
EndGlobal

1370
Gallium.sln.DotSettings Normal file

File diff suppressed because it is too large Load diff

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) Moonfire Games
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
README.md Normal file
View file

@ -0,0 +1,4 @@
Gallium CIL
===========
A small Entity-Component-System (ECS) that is built around LINQ calls and IEnumerable<Entity> objects.

3
commitlint.config.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
};

12308
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

21
package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "gallium-cil",
"version": "1.0.0",
"private": true,
"scripts": {
"prepare": "husky install"
},
"devDependencies": {
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@semantic-release/gitlab": "^6.2.2",
"@semantic-release/npm": "^7.1.3",
"commitlint-gitlab-ci": "^0.0.4",
"husky": "^7.0.2",
"semantic-release": "^17.4.7",
"semantic-release-dotnet": "^1.0.0",
"semantic-release-nuget": "^1.1.0"
}
}

20
release.config.js Normal file
View file

@ -0,0 +1,20 @@
module.exports = {
branches: ["main"],
message: "chore(release): v${nextRelease.version}\n\n${nextRelease.notes}",
plugins: [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"semantic-release-dotnet",
[
"semantic-release-nuget",
{
packArguments: ["--include-symbols", "--include-source"],
pushFiles: ["bin/*.nupkg"],
},
],
"@semantic-release/changelog",
"@semantic-release/git",
"@semantic-release/gitlab",
],
};

View file

@ -0,0 +1,213 @@
using System;
using Xunit;
using Xunit.Abstractions;
namespace Gallium.Tests
{
public class EntityTests : GalliumTestsBase
{
public EntityTests(ITestOutputHelper output)
: base(output)
{
}
[Fact]
public void AddingComponentOnceWorks()
{
var component1 = new TestComponent1();
Entity entity1 = new Entity().Add(component1);
Assert.Equal(1, entity1.Count);
Assert.True(entity1.Has<TestComponent1>());
Assert.Equal(component1, entity1.Get<TestComponent1>());
}
[Fact]
public void AddingComponentViaInterfaceWork()
{
var component1 = new TestComponent3a();
Entity entity1 = new Entity().Add<ITestComponent3>(component1);
Assert.Equal(1, entity1.Count);
Assert.False(entity1.Has<TestComponent3a>());
Assert.True(entity1.Has<ITestComponent3>());
Assert.Equal(component1, entity1.Get<ITestComponent3>());
}
[Fact]
public void AddingTwiceThrowsException()
{
var component1 = new TestComponent1();
Exception exception = Assert.Throws<ArgumentException>(
() => new Entity()
.Add(component1)
.Add(component1));
Assert.Equal(
"An element with the same type "
+ "(Gallium.Tests.TestComponent1)"
+ " already exists. (Parameter 'component')",
exception.Message);
}
[Fact]
public void CopyEntityAlsoCopiesComponents()
{
var component1 = new TestComponent1();
Entity entity1 = new Entity().Add(component1);
Entity entity2 = entity1.Copy();
Assert.Equal(1, entity2.Count);
Assert.True(entity2.Has<TestComponent1>());
Assert.Equal(component1, entity2.Get<TestComponent1>());
}
[Fact]
public void ExactCopyEntityAlsoCopiesComponents()
{
var component1 = new TestComponent1();
Entity entity1 = new Entity().Add(component1);
Entity entity2 = entity1.ExactCopy();
Assert.Equal(1, entity2.Count);
Assert.True(entity2.Has<TestComponent1>());
Assert.Equal(component1, entity2.Get<TestComponent1>());
}
[Fact]
public void GetOptionalWorks()
{
var component1 = new TestComponent1();
Entity entity1 = new Entity().Add(component1);
Assert.Equal(component1, entity1.GetOptional<TestComponent1>());
Assert.Null(entity1.GetOptional<TestComponent2>());
}
[Fact]
public void HasReturnsFalseIfNotRegistered()
{
var entity1 = new Entity();
Assert.False(entity1.Has<TestComponent1>());
}
[Fact]
public void NewEntityHasNoComponents()
{
var entity1 = new Entity();
Assert.Equal(0, entity1.Count);
}
[Fact]
public void RemoveAlreadyMissingComponentWorks()
{
var entity1 = new Entity();
Assert.Equal(0, entity1.Count);
Entity entity2 = entity1.Remove<TestComponent1>();
Assert.Equal(0, entity1.Count);
Assert.Equal(0, entity2.Count);
}
[Fact]
public void RemoveComponentDoesNotRemoveFromCopies()
{
var component1 = new TestComponent1();
Entity entity1 = new Entity().Add(component1);
Entity entity2 = entity1.ExactCopy();
Entity entity3 = entity1.Copy();
Assert.Equal(1, entity1.Count);
Assert.Equal(1, entity2.Count);
Assert.Equal(1, entity3.Count);
Entity entity1A = entity1.Remove<TestComponent1>();
Assert.Equal(1, entity1.Count);
Assert.Equal(0, entity1A.Count);
Assert.Equal(1, entity2.Count);
Assert.Equal(1, entity3.Count);
}
[Fact]
public void RemoveComponentWorks()
{
var component1 = new TestComponent1();
Entity entity1 = new Entity().Add(component1);
Assert.Equal(1, entity1.Count);
Entity entity2 = entity1.Remove<TestComponent1>();
Assert.Equal(1, entity1.Count);
Assert.Equal(0, entity2.Count);
}
[Fact]
public void SettingComponentsWorks()
{
var component1 = new TestComponent3a();
var component2 = new TestComponent3b();
Entity entity1 = new Entity()
.Set<ITestComponent3>(component1)
.Set<ITestComponent3>(component1)
.Set<ITestComponent3>(component2);
Assert.Equal(1, entity1.Count);
Assert.Equal(component2, entity1.Get<ITestComponent3>());
}
[Fact]
public void TryGetWorks()
{
var component1 = new TestComponent1();
Entity entity1 = new Entity().Add(component1);
bool result1 = entity1.TryGet(out TestComponent1 value1);
Assert.True(result1);
Assert.Equal(component1, value1);
bool result2 = entity1.TryGet(out TestComponent2 _);
Assert.False(result2);
}
[Fact]
public void TwoCopiesHaveDifferentIds()
{
var entity1 = new Entity();
Entity entity2 = entity1.Copy();
Assert.NotEqual(entity1.Id, entity2.Id);
Assert.NotEqual(entity1, entity2);
Assert.False(entity1 == entity2);
}
[Fact]
public void TwoEntitiesHaveDifferentIds()
{
var entity1 = new Entity();
var entity2 = new Entity();
Assert.NotEqual(entity1.Id, entity2.Id);
Assert.NotEqual(entity1, entity2);
Assert.False(entity1 == entity2);
}
[Fact]
public void TwoExactCopiesHaveDifferentIds()
{
var entity1 = new Entity();
Entity entity2 = entity1.ExactCopy();
Assert.Equal(entity1.Id, entity2.Id);
Assert.Equal(entity1, entity2);
Assert.True(entity1 == entity2);
}
}
}

View file

@ -0,0 +1,242 @@
using System.Linq;
using Xunit;
namespace Gallium.Tests
{
public class EnumerableEntityTests
{
[Fact]
public void ForComponentsC1()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent2()),
new Entity()
.Add("3")
.Add(new TestComponent1()),
};
Assert.Equal(
new[] { "1!", "2", "3!" },
entities
.ForEachEntity<TestComponent1>(
(e, _) => e.Set(e.Get<string>() + "!"))
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void ForComponentsC2()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent2()),
new Entity()
.Add("3")
.Add(new TestComponent1())
.Add(new TestComponent2()),
};
Assert.Equal(
new[] { "1", "2", "3!" },
entities
.ForEachEntity<TestComponent1, TestComponent2>(
(e, _, _) => e.Set(e.Get<string>() + "!"))
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void ForComponentsC3()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent2())
.Add<ITestComponent3>(new TestComponent3b()),
new Entity()
.Add("3")
.Add<ITestComponent3>(new TestComponent3a())
.Add(new TestComponent1())
.Add(new TestComponent2()),
};
Assert.Equal(
new[] { "1", "2", "3-TestComponent3a" },
entities
.ForEachEntity<TestComponent1, TestComponent2,
ITestComponent3>(
(e, _, _, t) => e.Set(
e.Get<string>() + "-" + t.GetType().Name))
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void HasComponentsC1()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent2()),
new Entity()
.Add("3")
.Add(new TestComponent1()),
};
Assert.Equal(
new[] { "1", "3" },
entities
.HasComponents<TestComponent1>()
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void HasComponentsC2()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent2())
.Add(new TestComponent1()),
new Entity()
.Add("3")
.Add(new TestComponent1()),
};
Assert.Equal(
new[] { "2" },
entities
.HasComponents<TestComponent1, TestComponent2>()
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void HasComponentsC3()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1())
.Add<ITestComponent3>(new TestComponent3b())
.Add(new TestComponent2()),
new Entity()
.Add("2")
.Add(new TestComponent2())
.Add(new TestComponent1()),
new Entity()
.Add("3")
.Add<ITestComponent3>(new TestComponent3a()),
};
Assert.Equal(
new[] { "1" },
entities
.HasComponents<TestComponent1, TestComponent2,
ITestComponent3>()
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void NotComponentsC1()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent2()),
new Entity()
.Add("3")
.Add(new TestComponent1()),
};
Assert.Equal(
new[] { "2" },
entities
.NotComponents<TestComponent1>()
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void NotComponentsC2()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent2())
.Add(new TestComponent1()),
new Entity()
.Add("3"),
};
Assert.Equal(
new[] { "1", "3" },
entities
.NotComponents<TestComponent1, TestComponent2>()
.Select(x => x.Get<string>())
.ToArray());
}
[Fact]
public void NotComponentsC3()
{
Entity[] entities =
{
new Entity()
.Add("1")
.Add(new TestComponent1()),
new Entity()
.Add("2")
.Add(new TestComponent1())
.Add(new TestComponent2())
.Add<ITestComponent3>(new TestComponent3b()),
new Entity()
.Add("3")
.Add<ITestComponent3>(new TestComponent3a()),
};
Assert.Equal(
new string[] { "1", "3" },
entities
.NotComponents<TestComponent1, TestComponent2,
ITestComponent3>()
.Select(x => x.Get<string>())
.ToArray());
}
}
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<RootNamespace>Gallium.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"/>
<PackageReference Include="Serilog.Sinks.XUnit" Version="2.0.4"/>
<PackageReference Include="xunit" Version="2.4.1"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Gallium\Gallium.csproj"/>
</ItemGroup>
</Project>

View file

@ -0,0 +1,40 @@
using Serilog;
using Serilog.Core;
using Xunit.Abstractions;
namespace Gallium.Tests
{
/// <summary>
/// Common initialization logic for Gallium-based tests including setting
/// up containers, logging, and Serilog.
/// </summary>
public abstract class GalliumTestsBase
{
protected GalliumTestsBase(ITestOutputHelper output)
{
this.Output = output;
// Set up logging.
const string Template =
"[{Level:u3}] "
+ "({SourceContext}) {Message}"
+ "{NewLine}{Exception}";
this.Logger = new LoggerConfiguration()
.WriteTo.TestOutput(
output,
outputTemplate: Template)
.CreateLogger();
}
/// <summary>
/// Gets the output for the tests.
/// </summary>
public ITestOutputHelper Output { get; }
/// <summary>
/// Gets the logger used to report messages about the test.
/// </summary>
protected Logger Logger { get; }
}
}

View file

@ -0,0 +1,6 @@
namespace Gallium.Tests
{
public interface ITestComponent3
{
}
}

View file

@ -0,0 +1,6 @@
namespace Gallium.Tests
{
public class TestComponent1
{
}
}

View file

@ -0,0 +1,6 @@
namespace Gallium.Tests
{
public class TestComponent2
{
}
}

View file

@ -0,0 +1,6 @@
namespace Gallium.Tests
{
public class TestComponent3a : ITestComponent3
{
}
}

View file

@ -0,0 +1,6 @@
namespace Gallium.Tests
{
public class TestComponent3b : ITestComponent3
{
}
}

300
src/Gallium/Entity.cs Normal file
View file

@ -0,0 +1,300 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
namespace Gallium
{
/// <summary>
/// A low-overhead entity with identification.
/// </summary>
public record Entity
{
/// <inheritdoc />
public virtual bool Equals(Entity? other)
{
if (ReferenceEquals(null, other))
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
return this.Id == other.Id;
}
/// <inheritdoc />
public override int GetHashCode()
{
return this.Id;
}
private ImmutableDictionary<Type, object> Components { get; set; }
/// <summary>
/// The internal ID to ensure the entities are unique. Since we are not
/// worried about serialization or using the identifiers from one call
/// to another, we can use a simple interlocked identifier instead of
/// a factory or provider method.
/// </summary>
private static int nextId;
public Entity()
: this(Interlocked.Increment(ref nextId))
{
}
private Entity(int id)
{
this.Id = id;
this.Components = ImmutableDictionary.Create<Type, object>();
}
/// <summary>
/// Gets a value indicating whether the entity has a specific type of
/// component registered.
/// </summary>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>True if the type exists, otherwise false.</returns>
public bool Has<TType>()
{
return this.Has(typeof(TType));
}
/// <summary>
/// Gets a value indicating whether the entity has a specific type of
/// component registered.
/// </summary>
/// <param name="type">The component type.</param>
/// <returns>True if the type exists, otherwise false.</returns>
public bool Has(Type type)
{
return this.Components.ContainsKey(type);
}
/// <summary>
/// Retrieves a registered component of the given type.
/// </summary>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>The registered object.</returns>
public TType Get<TType>()
{
return (TType)this.Components[typeof(TType)];
}
/// <summary>
/// Retrieves a registered component of the given type and casts it to
/// TType.
/// </summary>
/// <param name="type">The component key.</param>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>The registered object.</returns>
public TType Get<TType>(Type type)
{
return (TType)this.Components[type];
}
/// <summary>
/// Gets the number of components registered in the entity.
/// </summary>
public int Count => this.Components.Count;
/// <summary>
/// Gets the given component type if inside the entity, otherwise the
/// default avlue.
/// </summary>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>The found component or default (typically null).</returns>
public TType? GetOptional<TType>()
{
return this.Has<TType>() ? this.Get<TType>() : default;
}
/// <summary>
/// Attempts to get the value, if present. If not, this returns false
/// and the value is undefined. Otherwise, this method returns true
/// and the actual value inside that variable.
/// </summary>
/// <param name="value">The value if contained in the entity.</param>
/// <typeparam name="T1">The component type.</typeparam>
/// <returns>True if found, otherwise false.</returns>
public bool TryGet<T1>(out T1 value)
{
if (this.Has<T1>())
{
value = this.Get<T1>();
return true;
}
value = default!;
return false;
}
/// <summary>
/// Attempts to get the values, if present. If not, this returns false
/// and the value is undefined. Otherwise, this method returns true
/// and the actual value inside that variable.
/// </summary>
/// <param name="value1">The value if contained in the entity.</param>
/// <param name="value2">The value if contained in the entity.</param>
/// <typeparam name="T1">The first component type.</typeparam>
/// <typeparam name="T2">The second component type.</typeparam>
/// <returns>True if found, otherwise false.</returns>
public bool TryGet<T1, T2>(out T1 value1, out T2 value2)
{
if (this.Has<T1>() && this.Has<T2>())
{
value1 = this.Get<T1>();
value2 = this.Get<T2>();
return true;
}
value1 = default!;
value2 = default!;
return false;
}
/// <summary>
/// Sets the component in the entity, regardless if there was a
/// component already registered.
/// </summary>
/// <param name="component">The component to register.</param>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>The entity for chaining.</returns>
/// <exception cref="ArgumentNullException"></exception>
public Entity Set<TType>(TType component)
{
if (component == null)
{
throw new ArgumentNullException(nameof(component));
}
if (this.Components.TryGetValue(typeof(TType), out object? value)
&& value is TType
&& value.Equals(component))
{
return this;
}
return this with
{
Components = this.Components.SetItem(typeof(TType), component),
};
}
/// <summary>
/// Adds a component to the entity.
/// </summary>
/// <param name="component">The component to register.</param>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>
/// The same entity if the component is already registered, otherwise a
/// cloned entity with the new component.
/// </returns>
/// <exception cref="ArgumentNullException"></exception>
public Entity Add<TType>(TType component)
{
if (component == null)
{
throw new ArgumentNullException(nameof(component));
}
if (this.Has<TType>())
{
throw new ArgumentException(
"An element with the same type ("
+ typeof(TType).FullName
+ ") already exists.",
nameof(component));
}
if (this.Components.TryGetValue(typeof(TType), out object? value)
&& value is TType
&& value.Equals(component))
{
return this;
}
return this with
{
Components = this.Components.Add(typeof(TType), component),
};
}
/// <summary>
/// Removes a component to the entity.
/// </summary>
/// <typeparam name="TType">The component type.</typeparam>
/// <returns>
/// The same entity if the component is already removed, otherwise a
/// cloned entity without the new component.
/// </returns>
/// <exception cref="ArgumentNullException"></exception>
public Entity Remove<TType>()
{
return this.Remove(typeof(TType));
}
/// <summary>
/// Removes a component to the entity.
/// </summary>
/// <returns>
/// The same entity if the component is already removed, otherwise a
/// cloned entity without the new component.
/// </returns>
/// <param name="type">The component type to remove.</param>
public Entity Remove(Type type)
{
if (!this.Has(type))
{
return this;
}
return this with
{
Components = this.Components.Remove(type),
};
}
/// <summary>
/// Gets the identifier of the entity. This should be treated as an
/// opaque field.
/// </summary>
public int Id { get; private set; }
/// <summary>
/// Creates a copy of the entity, including copying the identifier.
/// </summary>
/// <returns></returns>
public Entity ExactCopy()
{
return this with { };
}
/// <summary>
/// Creates a copy of the entity, including components, but with a new
/// identifier.
/// </summary>
/// <returns></returns>
public Entity Copy()
{
return this with
{
Id = Interlocked.Increment(ref nextId),
};
}
/// <summary>
/// Retrieves a list of the component types currently registered in the
/// Entity.
/// </summary>
/// <returns>An enumerable of the various component keys.</returns>
public IEnumerable<Type> GetComponentTypes()
{
return this.Components.Keys;
}
}
}

View file

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EE = System.Collections.Generic.IEnumerable<Gallium.Entity>;
namespace Gallium
{
public static class EnumerableEntityExtensions
{
public static EE ForEachEntity<T1, T2>(
this EE entities,
Func<Entity, T1, T2, Entity> lambda)
{
return entities
.Select(
x => !x.Has<T1>() || !x.Has<T2>()
? x
: lambda(x, x.Get<T1>(), x.Get<T2>()));
}
public static EE ForEachEntity<T1, T2, T3>(
this EE entities,
Func<Entity, T1, T2, T3, Entity> lambda)
{
return entities
.Select(
x => !x.Has<T1>() || !x.Has<T2>() || !x.Has<T3>()
? x
: lambda(x, x.Get<T1>(), x.Get<T2>(), x.Get<T3>()));
}
public static EE ForEntities<T1, T2>(
this EE entities,
Func<EE, EE> lambda)
{
List<Entity> list = entities.ToList();
EE has = lambda(list.HasComponents<T1, T2>());
EE hasNot = list.NotComponents<T1, T2>();
return hasNot.Union(has);
}
public static EE ForEntities<T1, T2, T3>(
this EE entities,
Func<EE, EE> lambda)
{
List<Entity> list = entities.ToList();
EE has = lambda(list.HasComponents<T1, T2, T3>());
EE hasNot = list.NotComponents<T1, T2, T3>();
return hasNot.Union(has);
}
public static EE ForEntities<T1, T2, T3, T4>(
this EE entities,
Func<EE, EE> lambda)
{
List<Entity> list = entities.ToList();
EE has = lambda(list.HasComponents<T1, T2, T3, T4>());
EE hasNot = list.NotComponents<T1, T2, T3, T4>();
return hasNot.Union(has);
}
public static EE HasComponents<T1, T2>(this EE entities)
{
return entities
.Where(x => x.Has<T1>() && x.Has<T2>());
}
public static EE HasComponents<T1, T2, T3>(this EE entities)
{
return entities
.Where(x => x.Has<T1>() && x.Has<T2>() && x.Has<T3>());
}
public static EE HasComponents<T1, T2, T3, T4>(this EE entities)
{
return entities
.Where(
x => x.Has<T1>()
&& x.Has<T2>()
&& x.Has<T3>()
&& x.Has<T4>());
}
public static EE NotComponents<T1, T2>(this EE entities)
{
return entities
.Where(x => !x.Has<T1>() || !x.Has<T2>());
}
public static EE NotComponents<T1, T2, T3>(this EE entities)
{
return entities
.Where(x => !x.Has<T1>() || !x.Has<T2>() || !x.Has<T3>());
}
public static EE NotComponents<T1, T2, T3, T4>(this EE entities)
{
return entities
.Where(
x => !x.Has<T1>()
|| !x.Has<T2>()
|| !x.Has<T3>()
|| !x.Has<T4>());
}
}
}

View file

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using EE = System.Collections.Generic.IEnumerable<Gallium.Entity>;
namespace Gallium
{
public static class EnumerableEntityExtensions1
{
public static EE ForEachEntity<T1>(
this EE entities,
Func<Entity, T1, Entity> lambda)
{
return entities
.Select(x => x.Has<T1>() ? lambda(x, x.Get<T1>()) : x);
}
/// <summary>
/// Runs an inner set of operation for entities that have a specific
/// component.
/// </summary>
/// <param name="entities">The input to scan.</param>
/// <param name="lambda">
/// The list operation for the ones that have those
/// components.
/// </param>
/// <typeparam name="T1">The type of the first component.</typeparam>
/// <returns>
/// A combined list of components, with entities without the component
/// coming first and the ones with the component coming last after being
/// processed by the lambda.
/// </returns>
public static EE ForEntities<T1>(
this EE entities,
Func<EE, EE> lambda)
{
List<Entity> list = entities.ToList();
EE has = lambda(list.HasComponents<T1>());
EE hasNot = list.NotComponents<T1>();
return hasNot.Union(has);
}
public static EE HasComponents<T1>(this EE entities)
{
return entities
.Where(x => x.Has<T1>());
}
public static EE NotComponents<T1>(this EE entities)
{
return entities
.Where(x => !x.Has<T1>());
}
public static EE WhereEntities<T1>(
this EE entities,
Func<Entity, T1, bool> lambda,
bool includeWithoutComponent = true)
{
return entities
.Where(
x => x.Has<T1>()
? lambda(x, x.Get<T1>())
: includeWithoutComponent);
}
}
}

View file

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Gallium
{
public static class MergeEnumerableEntityExtensions
{
/// <summary>
/// Merges two sets of entities using the identifier to determine which
/// entities are the same. The `merge` function takes both of the
/// entities with the Entity from the `input` first and the one from
/// `other` second. The returning entity is put into the collection. If
/// an entity from the input is not found in other, then it is just
/// passed on.
/// </summary>
/// <param name="input">The enumerable of entities to merge to.</param>
/// <param name="other">The collection of entities to merge from.</param>
/// <param name="merge">The callback to merge the two.</param>
/// <returns>An sequence of entities, merged and unmerged.</returns>
public static IEnumerable<Entity> MergeEntities(
this IEnumerable<Entity> input,
ICollection<Entity> other,
Func<Entity, Entity, Entity> merge)
{
return input
.Select(
entity =>
{
Entity? found = other.FirstOrDefault(y => y == entity);
return found == null
? entity
: merge(entity, found);
});
}
}
}