This commit is contained in:
2026-04-15 22:22:56 -04:00
commit 5906f248f4
90 changed files with 6345 additions and 0 deletions

17
.clang-format Normal file
View File

@@ -0,0 +1,17 @@
BasedOnStyle: LLVM
AlignAfterOpenBracket: DontAlign
AlignOperands: DontAlign
AlignTrailingComments:
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortFunctionsOnASingleLine: Inline
BreakConstructorInitializers: AfterColon
ColumnLimit: 0
ContinuationIndentWidth: 2
IndentCaseLabels: true
IndentWidth: 2
InsertBraces: true
KeepEmptyLinesAtTheStartOfBlocks: false
RemoveSemicolon: true
SpacesInLineCommentPrefix:
TabWidth: 2
UseTab: Always

415
.editorconfig Normal file
View File

@@ -0,0 +1,415 @@
# --------------------------------------------------------------------------- #
# Chickensoft C# Style — .editorconfig #
# --------------------------------------------------------------------------- #
# Godot-friendly coding style with a bit of Dart-style flair thrown in. #
# --------------------------------------------------------------------------- #
# #
# #
# ╓╗_▄╗_╓▄_ #
# ▄▄╟▓▓▓▓▓▓▓▓ #
# ╙▓▓▓▀▀╠╠╦╦╓,_ #
# ,φ╠╠╠╠╠╠╠╠╠╠▒╥ #
# φ╠╠╠╠╠╠╠╠╠╠╠╠╠╠╦ #
# @╠╠╫▌╠╟▌╠╠╠╠╠╠╠╠╠ #
# ╠╠╠▄▄▄▒╠╠╠╠╠╠╠╠╠╠b #
# ╠╠╨███▌╠╠╠╠╠╠╠▒╠╠▒_ ç╓ #
# ╠╠╠╠▒▒╠╠╠╠╠╠╠╠▒Å▄╠╬▒φ╩ε #
# ╚╠╠╠╠╠╠╠╠╠╠╠▒█▒╫█Å╠╠╩ #
# ╠╠╠╠╠╠╠╠╠╠╠╠╠╟╫▒╠╠╩ #
# ╙╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╜ #
# ╙╚╠╠╠╠╠╠╠╠╩╙ #
# ╒ µ #
# ▌ ▓ #
# ^▀▀ "▀ª #
# #
# #
# --------------------------------------------------------------------------- #
#
# Based on:
# - https://github.com/RehanSaeed/EditorConfig/blob/main/.editorconfig
# - https://gist.github.com/FaronBracy/155d8d7ad98b4ceeb526b9f47543db1b
# - various other gists floating around :)
#
# Have a problem? Encounter an issue?
# Come visit our Discord and let us know! https://discord.gg/MjA6HUzzAE
#
# Based on https://github.com/RehanSaeed/EditorConfig/blob/main/.editorconfig
# and https://gist.github.com/FaronBracy/155d8d7ad98b4ceeb526b9f47543db1b
# This file is the top-most EditorConfig file
root = true
# All Files
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
##########################################
# File Extension Settings
##########################################
# GDScript Files
[*.{gd,gdshader,gdshaderinc}]
indent_style = tab
# Visual Studio Solution Files
[*.sln]
indent_style = tab
# Visual Studio XML Project Files
[*.{csproj,vbproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
# XML Configuration Files
[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct}]
indent_size = 2
# JSON Files
[*.{json,json5,webmanifest}]
indent_size = 2
# YAML Files
[*.{yml,yaml}]
indent_size = 2
# Markdown Files
[*.{md,mdx}]
trim_trailing_whitespace = false
# Web Files
[*.{htm,html,js,jsm,ts,tsx,cjs,cts,ctsx,mjs,mts,mtsx,css,sass,scss,less,pcss,svg,vue}]
indent_size = 2
# Batch Files
[*.{cmd,bat}]
end_of_line = crlf
# Makefiles
[Makefile]
indent_style = tab
[*{_Generated.cs,.g.cs,.generated.cs}]
# Ignore a lack of documentation for generated code. Doesn't apply to builds,
# just to viewing generation output.
dotnet_diagnostic.CS1591.severity = none
##########################################
# Default .NET Code Style Severities
##########################################
[*.cs]
# Default Severity for all .NET Code Style rules below
dotnet_analyzer_diagnostic.severity = warning
##########################################
# Language Rules
##########################################
# .NET Style Rules
# "this." and "Me." qualifiers
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_property = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_event = false
# Language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = true:warning
dotnet_style_predefined_type_for_member_access = true:warning
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning
dotnet_style_readonly_field = true:warning
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning
# Expression-level preferences
dotnet_style_object_initializer = true:warning
dotnet_style_collection_initializer = true:warning
dotnet_style_explicit_tuple_names = true:warning
dotnet_style_prefer_inferred_tuple_names = true:warning
dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning
dotnet_style_prefer_auto_properties = true:warning
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_diagnostic.IDE0045.severity = suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_diagnostic.IDE0046.severity = suggestion
dotnet_style_prefer_compound_assignment = true:warning
dotnet_style_prefer_simplified_interpolation = true:warning
dotnet_style_prefer_simplified_boolean_expressions = true:warning
# Null-checking preferences
dotnet_style_coalesce_expression = true:warning
dotnet_style_null_propagation = true:warning
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
# Unused parameters and methods
dotnet_diagnostic.IDE0060.severity = warn
dotnet_diagnostic.IDE0051.severity = warn
# File header preferences
# Keep operators at end of line when wrapping.
dotnet_style_operator_placement_when_wrapping = end_of_line
csharp_style_prefer_null_check_over_type_check = true:warning
# Code block preferences
csharp_prefer_braces = true:warning
csharp_prefer_simple_using_statement = true:suggestion
dotnet_diagnostic.IDE0063.severity = suggestion
# C# Style Rules
# 'var' preferences
csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = true:warning
# Expression-bodied members
csharp_style_expression_bodied_methods = when_on_single_line:warning
csharp_style_expression_bodied_constructors = false:warning
csharp_style_expression_bodied_operators = true:warning
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning
csharp_style_expression_bodied_lambdas = true:warning
csharp_style_expression_bodied_local_functions = true:warning
# Pattern matching preferences
csharp_style_pattern_matching_over_is_with_cast_check = true:warning
csharp_style_pattern_matching_over_as_with_null_check = true:warning
csharp_style_prefer_switch_expression = true:warning
csharp_style_prefer_pattern_matching = true:warning
csharp_style_prefer_not_pattern = true:warning
# Expression-level preferences
csharp_style_inlined_variable_declaration = true:warning
csharp_prefer_simple_default_expression = true:warning
csharp_style_pattern_local_over_anonymous_function = true:warning
csharp_style_deconstructed_variable_declaration = true:warning
csharp_style_prefer_index_operator = true:warning
csharp_style_prefer_range_operator = true:warning
csharp_style_implicit_object_creation_when_type_is_apparent = true:warning
# "Null" checking preferences
csharp_style_throw_expression = true:warning
csharp_style_conditional_delegate_call = true:warning
# 'using' directive preferences
csharp_using_directive_placement = inside_namespace:warning
# Use discard variable for unused expression values.
csharp_style_unused_value_expression_statement_preference = discard_variable
csharp_style_unused_value_assignment_preference = discard_variable
##########################################
# Formatting Rules
##########################################
# .NET formatting rules
# Organize using directives
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
# Dotnet namespace options
#
# We don't care about namespaces matching folder structure. Games and apps
# are complicated and you are free to organize them however you like. Change
# this if you want to enforce it.
dotnet_style_namespace_match_folder = false
dotnet_diagnostic.IDE0130.severity = none
# C# formatting rules
# Newline options
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation options
csharp_indent_switch_labels = true
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_labels = no_change
csharp_indent_block_contents = true
csharp_indent_braces = false
# Spacing options
csharp_space_after_cast = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_between_parentheses = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_around_binary_operators = before_and_after
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_after_comma = true
csharp_space_before_comma = false
csharp_space_after_dot = false
csharp_space_before_dot = false
csharp_space_after_semicolon_in_for_statement = true
csharp_space_before_semicolon_in_for_statement = false
csharp_space_around_declaration_statements = false
csharp_space_before_open_square_brackets = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_square_brackets = false
# Wrap options
csharp_preserve_single_line_statements = false
csharp_preserve_single_line_blocks = true
# Namespace options
csharp_style_namespace_declarations = file_scoped:warning
##########################################
# Unnecessary Code Rules
##########################################
# .NET Unnecessary code rules
dotnet_code_quality_unused_parameters = non_public:suggestion
dotnet_remove_unnecessary_suppression_exclusions = none
##########################################
# .NET Naming Rules
##########################################
##########################################
# Chickensoft Naming Conventions & Styles
# These deviate heavily from Microsoft's Official Naming Conventions.
##########################################
# Allow underscores in names.
dotnet_diagnostic.CA1707.severity = none
# Styles
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
dotnet_naming_style.upper_case_style.capitalization = all_upper
dotnet_naming_style.upper_case_style.word_separator = _
dotnet_naming_style.camel_case_style.capitalization = camel_case
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
# Use uppercase for all constant fields.
dotnet_naming_rule.constants_uppercase.severity = suggestion
dotnet_naming_rule.constants_uppercase.symbols = constant_fields
dotnet_naming_rule.constants_uppercase.style = upper_case_style
dotnet_naming_symbols.constant_fields.applicable_kinds = field
dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
dotnet_naming_symbols.constant_fields.required_modifiers = const
# Non-public fields should be _camelCase
dotnet_naming_rule.non_public_fields_under_camel.severity = suggestion
dotnet_naming_rule.non_public_fields_under_camel.symbols = non_public_fields
dotnet_naming_rule.non_public_fields_under_camel.style = camel_case_underscore_style
dotnet_naming_symbols.non_public_fields.applicable_kinds = field
dotnet_naming_symbols.non_public_fields.required_modifiers =
dotnet_naming_symbols.non_public_fields.applicable_accessibilities = private,private_protected,internal,protected,protected_internal
# Public fields should be PascalCase
dotnet_naming_rule.public_fields_pascal.severity = suggestion
dotnet_naming_rule.public_fields_pascal.symbols = public_fields
dotnet_naming_rule.public_fields_pascal.style = pascal_case_style
dotnet_naming_symbols.public_fields.applicable_kinds = field
dotnet_naming_symbols.public_fields.required_modifiers =
dotnet_naming_symbols.public_fields.applicable_accessibilities = public
# Async methods should have "Async" suffix.
# Disabled because it makes tests too verbose.
# dotnet_naming_style.end_in_async.required_suffix = Async
# dotnet_naming_style.end_in_async.capitalization = pascal_case
# dotnet_naming_rule.methods_end_in_async.symbols = methods_async
# dotnet_naming_rule.methods_end_in_async.style = end_in_async
# dotnet_naming_rule.methods_end_in_async.severity = warning
# dotnet_naming_symbols.methods_async.applicable_kinds = method
# dotnet_naming_symbols.methods_async.required_modifiers = async
# dotnet_naming_symbols.methods_async.applicable_accessibilities = *
##########################################
# Other Naming Rules
##########################################
# All of the following must be PascalCase:
dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property
dotnet_naming_rule.element_rule.symbols = element_group
dotnet_naming_rule.element_rule.style = pascal_case_style
dotnet_naming_rule.element_rule.severity = warning
# Interfaces use PascalCase and are prefixed with uppercase 'I'
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
dotnet_naming_style.prefix_interface_with_i_style.capitalization = pascal_case
dotnet_naming_style.prefix_interface_with_i_style.required_prefix = I
dotnet_naming_symbols.interface_group.applicable_kinds = interface
dotnet_naming_rule.interface_rule.symbols = interface_group
dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style
dotnet_naming_rule.interface_rule.severity = warning
# Generics Type Parameters use PascalCase and are prefixed with uppercase 'T'
# https://docs.microsoft.com/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces
dotnet_naming_style.prefix_type_parameters_with_t_style.capitalization = pascal_case
dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T
dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter
dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group
dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style
dotnet_naming_rule.type_parameter_rule.severity = warning
# Function parameters use camelCase
# https://docs.microsoft.com/dotnet/standard/design-guidelines/naming-parameters
dotnet_naming_symbols.parameters_group.applicable_kinds = parameter
dotnet_naming_rule.parameters_rule.symbols = parameters_group
dotnet_naming_rule.parameters_rule.style = camel_case_style
dotnet_naming_rule.parameters_rule.severity = warning
# Anything not specified uses camel case.
dotnet_naming_rule.unspecified_naming.severity = warning
dotnet_naming_rule.unspecified_naming.symbols = unspecified
dotnet_naming_rule.unspecified_naming.style = camel_case_style
dotnet_naming_symbols.unspecified.applicable_kinds = *
dotnet_naming_symbols.unspecified.applicable_accessibilities = *
##########################################
# Chickensoft Rule Overrides
##########################################
# Allow protected fields.
dotnet_diagnostic.CA1051.severity = none
# Don't warn about checking values that are supposedly never null. Sometimes
# they are actually null.
dotnet_diagnostic.CS8073.severity = none
# Allow expression values to go unused, even without discard variable.
# Otherwise, using Moq would be way too verbose.
dotnet_diagnostic.IDE0058.severity = none
# Allow me to use the word Collection if I want.
dotnet_diagnostic.CA1711.severity = none
# Don't warn about using reserved keywords (e.g., methods named "On")
dotnet_diagnostic.CA1716.severity = none
# Don't warn about public methods that can be marked static
# (tests frequently don't access member data, and GoDotTest won't call static methods)
dotnet_code_quality.CA1822.api_surface = private
# No primary constructors — not supported well by tooling.
dotnet_diagnostic.IDE0290.severity = none
# Let me write dumb if statements for readability.
dotnet_diagnostic.IDE0046.severity = none
# DO make me populate a *switch expression*
dotnet_diagnostic.IDE0072.severity = warning
# Don't make me populate a *switch statement*
dotnet_diagnostic.IDE0010.severity = none
# Make local functions static
dotnet_diagnostic.IDE0062.severity = warning
# Don't make me use properties if I don't want to.
dotnet_diagnostic.IDE0032.severity = none

40
.gitattributes vendored Normal file
View File

@@ -0,0 +1,40 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf
# Image formats
*.bmp filter=lfs diff=lfs merge=lfs -text
*.dds filter=lfs diff=lfs merge=lfs -text
*.exr filter=lfs diff=lfs merge=lfs -text
*.hdr filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.tga filter=lfs diff=lfs merge=lfs -text
*.webp filter=lfs diff=lfs merge=lfs -text
# Audio and Video formats
*.mp3 filter=lfs diff=lfs merge=lfs -text
*.wav filter=lfs diff=lfs merge=lfs -text
*.ogg filter=lfs diff=lfs merge=lfs -text
*.ogx filter=lfs diff=lfs merge=lfs -text
*.ogv filter=lfs diff=lfs merge=lfs -text
# 3D formats
*.gltf filter=lfs diff=lfs merge=lfs -text
*.blend filter=lfs diff=lfs merge=lfs -text
*.blend1 filter=lfs diff=lfs merge=lfs -text
*.glb filter=lfs diff=lfs merge=lfs -text
*.dae filter=lfs diff=lfs merge=lfs -text
*.fbx filter=lfs diff=lfs merge=lfs -text
# Build
*.dll filter=lfs diff=lfs merge=lfs -text
# Packaging
*.zip filter=lfs diff=lfs merge=lfs -text
*.7z filter=lfs diff=lfs merge=lfs -text
*.gz filter=lfs diff=lfs merge=lfs -text
*.rar filter=lfs diff=lfs merge=lfs -text
*.tar filter=lfs diff=lfs merge=lfs -text
*.file filter=lfs diff=lfs merge=lfs -text
*.dylib filter=lfs diff=lfs merge=lfs -text

30
.github/workflows/spellcheck.yaml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: "🧑‍🏫 Spellcheck"
on:
push:
pull_request:
jobs:
# We previously used the cspell action, but it didn't seem to properly respect
# the cspell.json file.
spellcheck:
name: "🧑‍🏫 Spellcheck"
# Prevents duplicate workflows from running on PR's that originate from the
# repository itself.
if: >
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name !=
github.event.pull_request.base.repo.full_name
runs-on: ubuntu-latest
defaults:
run:
working-directory: "."
steps:
- uses: actions/checkout@v6
name: 🧾 Checkout
- uses: streetsidesoftware/cspell-action@v8
name: 📝 Check Spelling
with:
config: "./cspell.json"
incremental_files_only: false
root: "."

35
.github/workflows/version_change.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: "🗂 Version Change"
on:
workflow_dispatch:
inputs:
version:
description: "Version (no 'v' prefix)"
required: true
jobs:
create_version_pull_request:
name: "🗂 Create Version Pull Request"
runs-on: ubuntu-latest
steps:
- name: "🧾 Checkout"
uses: actions/checkout@v6
- name: "📝 Change Version"
uses: vers-one/dotnet-project-version-updater@v1.7
with:
file: "ChickenGameTest.csproj"
version: ${{ github.event.inputs.version }}
- name: "⤴️ Create Pull Request"
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: version/${{ github.event.inputs.version }}
commit-message: >
update version to ${{ github.event.inputs.version }}
title: >
"chore(project): update version to
${{ github.event.inputs.version }}"
body: >
chore(project): update the version to
${{ github.event.inputs.version }}.

87
.github/workflows/visual_tests.yaml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: 🖼 Visual Tests
on:
push:
pull_request:
jobs:
visual_tests:
name: 🖼 Visual Tests with ${{ matrix.render-driver }}
runs-on: ubuntu-latest
# Only run the workflow if it's not a PR or if it's a PR from a fork.
# This prevents duplicate workflows from running on PR's that originate
# from the repository itself.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
DOTNET_NOLOGO: true
strategy:
# Don't cancel other runners if one fails.
fail-fast: false
matrix:
# Put the rendering drivers you want to use for tests here.
render-driver: [vulkan] # also: opengl3
defaults:
run:
# Use bash shells on all platforms.
shell: bash
steps:
- name: 🧾 Checkout
uses: actions/checkout@v6
with:
# If using git-lfs (large file storage), this ensures that your files
# are checked out properly.
lfs: true
# Make sure any git submodules are checked out as well.
submodules: 'recursive'
- name: 💽 Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
# Use the .NET SDK from global.json in the root of the repository.
global-json-file: global.json
- name: 📦 Restore Dependencies
run: dotnet restore
- name: 💾 Add Graphics Driver Emulators Source
run: |
sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
sudo add-apt-repository -n ppa:kisak/kisak-mesa
- name: 💾 Install Graphics Driver Emulators
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: mesa-vulkan-drivers binutils x11-xserver-utils
version: 1.0
- name: 🤖 Setup Godot
uses: chickensoft-games/setup-godot@v2
with:
# Version must include major, minor, and patch, and be >= 4.0.0
# Pre-release label is optional.
#
# In this case, we are using the version from global.json.
#
# This allows checks on renovatebot PR's to succeed whenever
# renovatebot updates the Godot SDK version.
version: global.json
- name: 🧑‍🔬 Generate .NET Bindings
run: godot --headless --build-solutions --quit || exit 0
- name: 🌋 Run Tests in Godot
run: |
xvfb-run godot --audio-driver Dummy --rendering-driver ${{ matrix.render-driver }} --run-tests --quit-on-finish
# The --coverage flag is used by GoDotTest to control the exit code
# of Godot by force-exiting through C#.
#
# Since Godot sometimes exits with non-zero exit codes (even on success),
# adding this flag to the above command may allow GoDotTest to ensure that
# this step will only fail when the tests fail, and not because Godot didn't
# exit cleanly.
#
# However, note that the --coverage flag can sometimes cause other failures
# by forcing Godot to exit before it can clean up its resources completely.
echo "Finished running tests in Godot with emulated ${{ matrix.render-driver }} graphics."

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
nupkg/
coverage/*
!coverage/.gdignore
.godot/
bin/
obj/
.generated/
.vs/
.DS_Store
*.old
*.translation
addons/*
!addons/.editorconfig
.addons/*

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "assets"]
path = assets
url = http://192.168.1.4:3000/Ronnie/FoodFactoryAssets

115
.vscode/chickensoft.code-snippets vendored Normal file
View File

@@ -0,0 +1,115 @@
{
// Place your workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Chickensoft AutoNode": {
"scope": "csharp",
"isFileTemplate": true,
"prefix": [ "autonode" ],
"body": [
"namespace ${1:$WORKSPACE_NAME};",
"",
"using Chickensoft.AutoInject;",
"using Chickensoft.Introspection;",
"using Godot;",
"",
"[Meta(typeof(IAutoNode))]",
"public partial class ${2:$TM_FILENAME_BASE} : ${3:Node} {",
" public override void _Notification(int what) => this.Notify(what);",
"$0",
"}",
""
],
"description": "Chickensoft AutoNode template"
},
"Chickensoft AutoInject Provision": {
"scope": "csharp",
"prefix": [ "provide" ],
"body": [
"${1:ProvisionType} IProvide<${1:ProvisionType}>.Value() => ${2:ProvisionValue};"
],
"description": "Chickensoft AutoInject IProvide implementation"
},
"Chickensoft AutoInject Dependency": {
"scope": "csharp",
"prefix": [ "depend" ],
"body": [
"[Dependency] ${1:public} ${2:DependencyType} ${3:DependencyValue} => this.DependOn<${2:DependencyType}>();"
],
"description": "Chickensoft AutoInject Dependency attribute"
},
"Chickensoft AutoProp": {
"scope": "csharp",
"prefix": [ "autoprop" ],
"body": [
"public IAutoProp<${1:PropType}> ${2:PublicPropName} => ${3:_privatePropName};",
"private readonly AutoProp<${1:PropType}> ${3:_privatePropName} = new($0);"
],
"description": "Chickensoft Collections AutoProp attribute"
},
"Chickensoft LogicBlock": {
"scope": "csharp",
"isFileTemplate": true,
"prefix": [ "logicblock", "lb" ],
"body": [
"namespace ${1:$WORKSPACE_NAME};",
"",
"using Chickensoft.Introspection;",
"using Chickensoft.LogicBlocks;",
"",
"[Meta, LogicBlock(typeof(State), Diagram = true)]",
"public partial class ${2:$TM_FILENAME_BASE} : LogicBlock<${2:$TM_FILENAME_BASE}.State> {",
" public override Transition GetInitialState() => To<${3:State}>();",
"$0",
" public abstract partial record State : StateLogic<State>;",
"}",
""
],
"description": "Chickensoft LogicBlock template"
},
"Chickensoft LogicBlock Inputs": {
"scope": "csharp",
"isFileTemplate": true,
"prefix": [ "logicblock-input", "lbin" ],
"body": [
"namespace ${1:$WORKSPACE_NAME};",
"",
"public partial class ${2:${TM_FILENAME_BASE/([a-zA-Z0-9_]*)([\\.]Input)/$1/}} {",
" public static class Input {",
" public readonly record struct ${3:MyInput};",
" }",
"}",
""
],
"description": "Chickensoft LogicBlock Input class"
},
"Chickensoft LogicBlock Outputs": {
"scope": "csharp",
"isFileTemplate": true,
"prefix": [ "logicblock-output", "lbout" ],
"body": [
"namespace ${1:$WORKSPACE_NAME};",
"",
"public partial class ${2:${TM_FILENAME_BASE/([a-zA-Z0-9_]*)([\\.]Output)/$1/}} {",
" public static class Output {",
" public readonly record struct ${3:MyOutput};",
" }",
"}",
""
],
"description": "Chickensoft LogicBlock Output class"
}
}

29
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,29 @@
{
"recommendations": [
"alfish.godot-files",
"bierner.emojisense",
"christian-kohler.path-intellisense",
"codezombiech.gitignore",
"DavidAnson.vscode-markdownlint",
"EditorConfig.EditorConfig",
"edwinhuish.better-comments-next",
"emeraldwalk.runonsave",
"fernandoescolar.vscode-solution-explorer",
"geequlim.godot-tools",
"gurumukhi.selected-lines-count",
"IBM.output-colorizer",
"jebbs.plantuml",
"josefpihrt-vscode.roslynator",
"ms-dotnettools.csharp",
"ms-dotnettools.vscode-dotnet-runtime",
"muhammad-sammy.csharp",
"oderwat.indent-rainbow",
"pflannery.vscode-versionlens",
"qcz.text-power-tools",
"selcukermaya.se-csproj-extensions",
"stkb.rewrap",
"streetsidesoftware.code-spell-checker",
"tintoy.msbuild-project-tools",
"yzhang.markdown-all-in-one",
]
}

34
.vscode/godot.code-snippets vendored Normal file
View File

@@ -0,0 +1,34 @@
{
// Place your workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Godot [Export]": {
"scope": "csharp",
"prefix": [ "export" ],
"body": [
"[Export] ${1:public} ${2:Node} ${3:MyExport} { get; set; } = default!;"
],
"description": "Godot [Export] attribute"
},
"Godot [Signal]": {
"scope": "csharp",
"prefix": [ "signal" ],
"body": [
"[Signal] public delegate void ${1:My}EventHandler($2);"
],
"description": "Godot [Signal] attribute"
}
}

107
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,107 @@
{
"version": "0.2.0",
"configurations": [
// For these launch configurations to work, you need to setup a GODOT
// environment variable. On mac or linux, this can be done by adding
// the following to your .zshrc, .bashrc, or .bash_profile file:
// export GODOT="/Applications/Godot.app/Contents/MacOS/Godot"
{
"name": "🕹 Debug Game",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-without-tests",
"program": "${env:GODOT}",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "integratedTerminal"
},
{
"name": "🕹 Debug Game (VSCodium)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-without-tests",
"program": "",
"internalConsoleOptions": "openOnSessionStart",
"pipeTransport": {
"debuggerPath": "${extensionInstallFolder:muhammad-sammy.csharp}/.debugger/netcoredbg/netcoredbg",
"pipeCwd": "${workspaceFolder}",
"pipeProgram": "${env:GODOT}",
"pipeArgs": [
"--debug"
]
},
"osx": {
"pipeTransport": {
// netcoredbg for Apple Silicon isn't included with the VSCodium C#
// extension. You must clone it, build it, and setup the path to it.
// You'll need homebrew, cmake, and clang installed.
//
// --------------------------------------------------------------- //
//
// git clone https://github.com/Samsung/netcoredbg.git
// cd netcoredbg
// mkdir build
// cd build
// CC=clang CXX=clang++ cmake .. -DCMAKE_INSTALL_PREFIX=$PWD/../bin
//
// In your ~/.zshrc file, add the following line and adjust the path:
//
// export NETCOREDBG="/path/to/netcoredbg/bin/netcoredbg"
//
"debuggerPath": "${env:NETCOREDBG}",
"pipeCwd": "${workspaceFolder}",
"pipeProgram": "${env:GODOT}",
"pipeArgs": [
"--debug"
]
}
},
},
// Debug the scene that matches the name of the currently open *.cs file
// (if there's a scene with the same name in the same directory).
{
"name": "🎭 Debug Current Scene",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build-without-tests",
"program": "${env:GODOT}",
"args": [
"${fileDirname}/${fileBasenameNoExtension}.tscn"
],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "integratedTerminal"
},
{
"name": "🧪 Debug Tests",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${env:GODOT}",
"args": [
// These command line flags are used by GoDotTest to run tests.
"--run-tests",
"--quit-on-finish"
],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "integratedTerminal"
},
{
"name": "🔬 Debug Current Test",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${env:GODOT}",
"args": [
// These command line flags are used by GoDotTest to run tests.
"--run-tests=${fileBasenameNoExtension}",
"--quit-on-finish"
],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "integratedTerminal"
},
]
}

187
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,187 @@
{
"[csharp]": {
"editor.codeActionsOnSave": {
"source.addMissingImports": "explicit",
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
},
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false
},
"emeraldwalk.runonsave": {
"commands": [
{
// run `clang-format` on Godot shader files when saving
"match": "\\.gdshader|\\.gdshaderinc$",
"cmd": "clang-format -i '${file}'"
}
]
},
"csharp.debug.justMyCode": false,
"csharp.debug.suppressJITOptimizations": true,
"csharp.semanticHighlighting.enabled": true,
"dotnet.backgroundAnalysis.analyzerDiagnosticsScope": "fullSolution",
"dotnet.backgroundAnalysis.compilerDiagnosticsScope": "fullSolution",
"dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true,
"dotnet.enableXamlTools": false,
"dotnet.formatting.organizeImportsOnFormat": true,
"dotnet.preferCSharpExtension": true,
// "dotnet.server.useOmnisharp": true, // You decide
"dotnetAcquisitionExtension.enableTelemetry": false,
"editor.semanticHighlighting.enabled": true,
// C# doc comment colorization gets lost with semantic highlighting, but we
// need semantic highlighting for proper syntax highlighting with record
// shorthand.
//
// Here's a workaround for doc comment highlighting from
// https://github.com/OmniSharp/omnisharp-vscode/issues/3816
"editor.tokenColorCustomizations": {
"[*]": {
// Themes that don't include the word "Dark" or "Light" in them.
// These are some bold colors that show up well against most dark and
// light themes.
//
// Change them to something that goes well with your preferred theme :)
"textMateRules": [
{
"scope": "comment.documentation",
"settings": {
"foreground": "#0091ff"
}
},
{
"scope": "comment.documentation.attribute",
"settings": {
"foreground": "#8480ff"
}
},
{
"scope": "comment.documentation.cdata",
"settings": {
"foreground": "#0091ff"
}
},
{
"scope": "comment.documentation.delimiter",
"settings": {
"foreground": "#aa00ff"
}
},
{
"scope": "comment.documentation.name",
"settings": {
"foreground": "#ef0074"
}
}
]
},
"[*Dark*]": {
// Themes that include the word "Dark" in them.
"textMateRules": [
{
"scope": "comment.documentation",
"settings": {
"foreground": "#608B4E"
}
},
{
"scope": "comment.documentation.attribute",
"settings": {
"foreground": "#C8C8C8"
}
},
{
"scope": "comment.documentation.cdata",
"settings": {
"foreground": "#E9D585"
}
},
{
"scope": "comment.documentation.delimiter",
"settings": {
"foreground": "#808080"
}
},
{
"scope": "comment.documentation.name",
"settings": {
"foreground": "#569CD6"
}
}
]
},
"[*Light*]": {
// Themes that include the word "Light" in them.
"textMateRules": [
{
"scope": "comment.documentation",
"settings": {
"foreground": "#008000"
}
},
{
"scope": "comment.documentation.attribute",
"settings": {
"foreground": "#282828"
}
},
{
"scope": "comment.documentation.cdata",
"settings": {
"foreground": "#808080"
}
},
{
"scope": "comment.documentation.delimiter",
"settings": {
"foreground": "#808080"
}
},
{
"scope": "comment.documentation.name",
"settings": {
"foreground": "#808080"
}
}
]
}
},
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"*": "$(capture).import, $(capture).uid"
},
"markdownlint.config": {
// Allow non-unique heading names so we don't break the changelog.
"MD024": false,
// Allow html in markdown.
"MD033": false
},
"markdownlint.ignore": [
"**/LICENSE"
],
"omnisharp.enableEditorConfigSupport": true,
"omnisharp.enableMsBuildLoadProjectsOnDemand": false,
"omnisharp.maxFindSymbolsItems": 3000,
"omnisharp.useModernNet": true,
// Remove these if you're happy with your terminal profiles.
"terminal.integrated.defaultProfile.windows": "Git Bash",
"terminal.integrated.profiles.windows": {
"Command Prompt": {
"icon": "terminal-cmd",
"path": [
"${env:windir}\\Sysnative\\cmd.exe",
"${env:windir}\\System32\\cmd.exe"
]
},
"Git Bash": {
"icon": "terminal",
"source": "Git Bash"
},
"PowerShell": {
"icon": "terminal-powershell",
"source": "PowerShell"
}
}
}

80
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,80 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"group": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
],
"problemMatcher": "$msCompile",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
}
},
{
"label": "build-without-tests",
"group": "build",
"command": "dotnet",
"type": "process",
"options": {
"env": {
"SKIP_TESTS": "1"
}
},
"args": [
"build",
],
"problemMatcher": "$msCompile",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
}
},
{
"label": "coverage",
"group": "test",
"command": "${workspaceFolder}/coverage.sh",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
},
},
{
"label": "build-solutions",
"group": "test",
"command": "dotnet restore; ${env:GODOT} --headless --build-solutions --quit || exit 0",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}"
},
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false
}
},
]
}

44
.zed/settings.json Normal file
View File

@@ -0,0 +1,44 @@
// Zed settings
//
// For information on how to configure Zed, see the Zed
// documentation: https://zed.dev/docs/configuring-zed
//
// To see all of Zed's default settings without changing your
// custom settings, run `zed: open default settings` from the
// command palette (cmd-shift-p / ctrl-shift-p)
{
"wrap_guides": [
80
],
"languages": {
"CSharp": {
"format_on_save": "on",
"formatter": "language_server",
// "code_actions_on_format": {
// "source.addMissingImports": true,
// "source.organizeImports": true,
// "source.fixAll": true
// },
"tab_size": 2
},
"JSON": {
"format_on_save": "on",
"formatter": "language_server"
},
"JSONC": {
"format_on_save": "on",
"formatter": "language_server"
}
},
"telemetry": {
"diagnostics": false,
"metrics": false
},
"file_types": {
"XML": [
"csproj",
"xsl",
"props"
]
}
}

77
ChickenGameTest.csproj Normal file
View File

@@ -0,0 +1,77 @@
<Project Sdk="Godot.NET.Sdk/4.6.2">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<RootNamespace>ChickenGameTest</RootNamespace>
<!-- Catch compiler-mismatch issues with the Introspection generator as early as possible -->
<WarningsAsErrors>CS9057</WarningsAsErrors>
<!-- Required for some nuget packages to work -->
<!-- godotengine/godot/issues/42271#issuecomment-751423827 -->
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<!-- To show generated files -->
<!-- <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> -->
<!--
<CompilerGeneratedFilesOutputPath>.generated</CompilerGeneratedFilesOutputPath>
-->
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
<Title>ChickenGameTest</Title>
<Version>1.0.0</Version>
<Description>ChickenGameTest</Description>
<Copyright>© 2024 SuperJrKing</Copyright>
<Authors>SuperJrKing</Authors>
<Company>SuperJrKing</Company>
<SkipTests Condition="'$(SKIP_TESTS)' != ''">true</SkipTests>
<RunTests>false</RunTests>
</PropertyGroup>
<PropertyGroup Condition="&#xA; ('$(Configuration)' == 'Debug' or '$(Configuration)' == 'ExportDebug')&#xA; and '$(SkipTests)' != 'true' ">
<RunTests>true</RunTests>
<DefineConstants>$(DefineConstants);RUN_TESTS</DefineConstants>
</PropertyGroup>
<ItemGroup>
<!-- Production dependencies go here! -->
<PackageReference Include="Chickensoft.GameTools" Version="3.1.6" />
<PackageReference Include="SjkScripts" Version="1.0.1" />
<PackageReference Include="System.IO.Abstractions" Version="22.1.0" />
<PackageReference Include="EnvironmentAbstractions" Version="5.0.0" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.6.0" PrivateAssets="all" OutputItemType="analyzer" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.3.54" />
<PackageReference Include="Chickensoft.AutoInject" Version="2.9.18" PrivateAssets="all" />
<PackageReference Include="Chickensoft.Collections" Version="3.1.4" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.57" />
<PackageReference Include="Chickensoft.Introspection" Version="3.0.2" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="3.0.2" PrivateAssets="all" OutputItemType="analyzer" />
<PackageReference Include="Chickensoft.Serialization" Version="3.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.8.46" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.20.0" />
<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" />
</ItemGroup>
<ItemGroup Condition="'$(RunTests)' == 'true'">
<!-- Test dependencies go here! -->
<!-- Dependencies added here will not be included in release builds. -->
<PackageReference Include="Chickensoft.GoDotTest" Version="2.0.27" />
<!-- Used to drive test scenes when testing visual code -->
<PackageReference Include="Chickensoft.GodotTestDriver" Version="3.1.56" />
<!-- Bring your own assertion library for tests! -->
<!-- We're using Shouldly for this example, but you can use anything. -->
<PackageReference Include="Shouldly" Version="4.3.0" />
<!-- LightMock is a mocking library that works without reflection. -->
<PackageReference Include="LightMock.Generator" Version="1.2.3" />
<!-- LightMoq is a Chickensoft package which makes it more like Moq. -->
<PackageReference Include="LightMoq" Version="0.1.0" />
</ItemGroup>
<ItemGroup Condition="'$(RunTests)' != 'true'">
<Compile Remove="test/**/*.cs" />
<None Remove="test/**/*" />
<EmbeddedResource Remove="test/**/*" />
</ItemGroup>
</Project>

19
ChickenGameTest.sln Normal file
View File

@@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChickenGameTest", "ChickenGameTest.csproj", "{9E95E51D-5AD1-416E-B464-2C3F9C99BFED}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9E95E51D-5AD1-416E-B464-2C3F9C99BFED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9E95E51D-5AD1-416E-B464-2C3F9C99BFED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9E95E51D-5AD1-416E-B464-2C3F9C99BFED}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{9E95E51D-5AD1-416E-B464-2C3F9C99BFED}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{9E95E51D-5AD1-416E-B464-2C3F9C99BFED}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{9E95E51D-5AD1-416E-B464-2C3F9C99BFED}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
# MIT License
Copyright (c) 2024 SuperJrKing
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.

222
README.md Normal file
View File

@@ -0,0 +1,222 @@
# ChickenGameTest
[![Chickensoft Badge][chickensoft-badge]][chickensoft-website] [![Discord][discord-badge]][discord] [![Read the docs][read-the-docs-badge]][docs] ![line coverage][line-coverage] ![branch coverage][branch-coverage]
C# game template for Godot 4 with debug launch configurations, testing (locally and on CI/CD), code coverage, dependency update checks, and spell check working out-of-the-box!
---
<p align="center">
<img alt="Cardboard Box with Chickensoft Logo" src="icon.png" width="200">
</p>
## 🥚 Getting Started
This template allows you to easily create a C# game for Godot 4. Microsoft's `dotnet` tool allows you to easily create, install, and use templates.
```sh
# Install this template
dotnet new install ChickenGameTest
# Generate a new project based on this template
dotnet new chickengame --name "MyGameName" --param:author "My Name"
cd MyGameName
dotnet build
```
## 💁 Getting Help
*Is this template broken? Encountering obscure C# build problems?* We'll be happy to help you in the [Chickensoft Discord server][discord].
## 🏝 Environment Setup
For the provided debug configurations and test coverage to work correctly, you must setup your development environment correctly. The [Chickensoft Setup Docs][setup-docs] describe how to setup your Godot and C# development environment, using Chickensoft's best practice recommendations.
### VSCode Settings
This template includes some Visual Studio Code settings in `.vscode/settings.json`. The settings facilitate terminal environments on Windows (Git Bash, PowerShell, Command Prompt) and macOS (zsh), as well as fixing some syntax colorization issues that Omnisharp suffers from. You'll also find settings that enable editor config support in Omnisharp and the .NET Roslyn analyzers for a more enjoyable coding experience.
> Please double-check that the provided VSCode settings don't conflict with your existing settings.
## .NET Versioning
The included [`global.json`](./global.json) specifies the version of the .NET SDK and `Godot.NET.Sdk` that the game should use. Using a `global.json` file allows [Renovatebot] to provide your repository with automatic dependency update pull requests whenever a new version of [GodotSharp] is released.
## 👷 Testing
An example test is included in `test/src/GameTest.cs` that demonstrates how to write a test for your package using [GoDotTest] and [godot-test-driver].
> [GoDotTest] is an easy-to-use testing framework for Godot and C# that allows you to run tests from the command line, collect code coverage, and debug tests in VSCode.
Tests run directly inside the game. The `.csproj` file is already pre-configured to prevent test scripts and test-only package dependencies from being included in release builds of your game!
On CI/CD, software graphics drivers from [mesa] emulate a virtual graphics device for Godot to render to, allowing you to run visual tests in a headless environment.
## 🏁 Application Entry Point
The `Main.tscn` and `Main.cs` scene and script file are the entry point of your game. In general, you probably won't need to modify these unless you're doing something highly custom.
If the game is running a release build, the `Main.cs` file will just immediately change the scene to `src/Game.tscn`. If the game is running in debug mode *and* GoDotTest has received the correct command line arguments to begin testing, the game will switch to the testing scene and hand off control to GoDotTest to run the game's tests.
In general, prefer editing `src/Game.tscn` over `src/Main.tscn`.
The provided debug configurations in `.vscode/launch.json` allow you to easily debug tests (or just the currently open test, provided its filename matches its class name).
## 🚦 Test Coverage
Code coverage requires a few `dotnet` global tools to be installed first. You should install these tools from the root of the project directory.
The `nuget.config` file in the root of the project allows the correct version of `coverlet` to be installed from the coverlet nightly distributions. Overriding the coverlet version will be required [until coverlet releases a stable version with the fixes that allow it to work with Godot 4][coverlet-issues].
```sh
dotnet tool install --global coverlet.console
dotnet tool update --global coverlet.console
dotnet tool install --global dotnet-reportgenerator-globaltool
dotnet tool update --global dotnet-reportgenerator-globaltool
```
> Running `dotnet tool update` for the global tool is often necessary on Apple Silicon computers to ensure the tools are installed correctly.
You can collect code coverage and generate coverage badges by running the bash script `coverage.sh` (on Windows, you can use the Git Bash shell that comes with git).
```sh
# Must give coverage script permission to run the first time it is used.
chmod +x ./coverage.sh
# Run code coverage:
./coverage.sh
```
You can also run test coverage through VSCode by opening the command palette and selecting `Tasks: Run Task` and then choosing `coverage`.
If you are having trouble with `coverlet` finding your .NET runtime on Windows, you can use the PowerShell Script `coverage.ps1` instead.
```ps
.\coverage.ps1
```
## ⏯ Running the Project
Several launch profiles are included for Visual Studio Code:
- 🕹 **Debug Game**
Runs the game in debug mode, allowing you to set breakpoints and inspect variables.
- 🎭 **Debug Current Scene**
Debugs the game and loads the scene with the **same name** and **in the same path** as the C# file that's actively selected in VSCode: e.g., a scene named `MyScene.tscn` must reside in the same directory as `MyScene.cs`, and you must have selected `MyScene.cs` as the active tab in VSCode before running the launch profile.
If GoDotTest is able to find a `.tscn` file with the same name in the same location, it will run the game in debug mode and load the scene.
> Naturally, Chickensoft recommends naming scenes after the C# script they use and keeping them in the same directory so that you can take advantage of this launch profile.
>
> ⚠️ It's very easy to rename a script class but forget to rename the scene file, or vice-versa. When that happens, this launch profile will pass in the *expected* name of the scene file based on the script's name, but Godot will fail to find a scene with that name since the script name and scene name are not the same.
- 🧪 **Debug Tests**
Runs the game in debug mode, specifying the command line flags needed by GoDotTest to run the tests. Debugging works the same as usual, allowing you to set breakpoints within the game's C# test files.
- 🔬 **Debug Current Test**
Debugs the game and loads the test class with the **same name** as the C# file that's actively selected in VSCode: e.g., a test file named `MyTest.cs` must contain a test class named `MyTest`, and you must have selected `MyTest.cs` as the active tab in VSCode before running the launch profile.
> ⚠️ It's very easy to rename a test class but forget to rename the test file, or vice-versa. When that happens, this launch profile will pass in the name of the file but GoDotTest will fail to find a class with that name since the filename and class name are not the same.
Note that each launch profile will trigger a build (see `./.vscode/tasks.json`) before debugging the game.
> ⚠️ **Important:** You must setup a `GODOT` environment variable for the launch configurations above. If you haven't done so already, please see the [Chickensoft Setup Docs][setup-docs].
## 🏭 CI/CD
This game includes various GitHub Actions workflows to help with development.
## 🌈 Shaders
You'll want to install [clang-format] and make sure it's available on your system `PATH` for automatic shader formatting to work whenever you save a `.gdshader` or `.gdshaderinc` file.
- [Install on Windows](https://superuser.com/a/1611210)
- On macOS, use [homebrew] and run `brew install clang-format`. You'll want to make sure that homebrew is able to update your shell's `PATH` so that the installed `clang-format` binary is available globally.
- Linux: you know what to do
> [!CAUTION]
> On Windows, you **must** logout and log back in (or restart your computer) after updating environment variables. On macOS/Linux, you may need to restart your application (if not logout and log back in) for the updated `PATH` to be recognized.
This template includes an updated version of the `.clang-format` file mentioned in the [Godot Shaders Style Guide](https://docs.godotengine.org/en/stable/tutorials/shaders/shaders_style_guide.html#applying-formatting-automatically).
For syntax highlighting, we recommend the [Godot Tools](https://marketplace.visualstudio.com/items?itemName=geequlim.godot-tools) extension since it provides functionality for other Godot engine features as well.
### 🚥 Tests
Tests run directly inside the GitHub runner machine (using [chickensoft-games/setup-godot]) on every push to the repository. If the tests fail to pass, the workflow will also fail to pass.
You can configure which simulated graphics environments (`vulkan` and/or `opengl3`) you want to run tests on in [`.github/workflows/visual_tests.yaml`](.github/workflows/visual_tests.yaml).
Currently, tests can only be run from the `ubuntu` runners. If you know how to make the workflow install mesa and a virtual window manager on macOS and Windows, we'd love to hear from you!
Tests are executed by running the Godot test project in `ChickenGameTest` from the command line and passing in the relevant arguments to Godot so that [GoDotTest] can discover and run tests.
### 🧑‍🏫 Spellcheck
A spell check runs on every push to the repository. The spellcheck workflow settings can be configured in [`.github/workflows/spellcheck.yaml`](.github/workflows/spellcheck.yaml).
The [Code Spell Checker][cspell] plugin for VSCode is recommended to help you catch typos before you commit them. If you need add a word to the dictionary or ignore a certain path, you can edit the project's `cspell.json` file.
You can also words to the local `cspell.json` file from VSCode by hovering over a misspelled word and selecting `Quick Fix...` and then `Add "{word}" to config: cspell.json`.
![Fix Spelling](docs/spelling_fix.png)
### 🗂 Version Change
The included workflow in [`.github/workflows/version_change.yaml`](.github/workflows/version_change.yaml) can be manually dispatched to open a pull request that replaces the version number in `ChickenGameTest.csproj` with the version you specify in the workflow's inputs.
![Version Change Workflow](docs/version_change.png)
### 📦 Publish to Nuget
The included workflow in [`.github/workflows/publish.yaml`](.github/workflows/publish.yaml) can be manually dispatched when you're ready to publish your package to Nuget.
> To publish to nuget, you need a repository or organization secret named `NUGET_API_KEY` that contains your Nuget API key. The `NUGET_API_KEY` must be a GitHub actions secret to keep it safe!
![Publish Workflow](docs/publish.png)
### 🏚 Renovatebot
This repository includes a [`renovate.json`](./renovate.json) configuration for use with [Renovatebot]. Renovatebot can automatically open pull requests to help you keep your dependencies up to date when it detects new dependency versions have been released. Because Godot has such a rapid release cycle, automating dependency updates can be a huge time saver if you're trying to stay on the latest version of Godot.
![Renovatebot Pull Request](docs/renovatebot_pr.png)
> Unlike Dependabot, Renovatebot is able to combine all dependency updates into a single pull request — a must-have for Godot C# repositories where each sub-project needs the same Godot.NET.Sdk versions. If dependency version bumps were split across multiple repositories, the builds would fail in CI.
The easiest way to add Renovatebot to your repository is to [install it from the GitHub Marketplace][get-renovatebot]. Note that you have to grant it access to each organization and repository you want it to monitor.
The included `renovate.json` includes a few configuration options to limit how often Renovatebot can open pull requests as well as regex's to filter out some poorly versioned dependencies to prevent invalid dependency version updates.
---
🐣 Package generated from a 🐤 Chickensoft Template — <https://chickensoft.games>
<!-- Links -->
<!-- Header -->
[chickensoft-badge]: https://chickensoft.games/img/badges/chickensoft_badge.svg
[chickensoft-website]: https://chickensoft.games
[discord-badge]: https://chickensoft.games/img/badges/discord_badge.svg
[discord]: https://discord.gg/gSjaPgMmYW
[read-the-docs-badge]: https://chickensoft.games/img/badges/read_the_docs_badge.svg
[docs]: https://chickensoft.games/docs
[line-coverage]: badges/line_coverage.svg
[branch-coverage]: badges/branch_coverage.svg
<!-- Article -->
[GoDotTest]: https://github.com/chickensoft-games/go_dot_test
[setup-docs]: https://chickensoft.games/docs/setup
[cspell]: https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker
[Renovatebot]: https://www.mend.io/free-developer-tools/renovate/
[get-renovatebot]: https://github.com/apps/renovate
[godot-test-driver]: https://github.com/derkork/godot-test-driver
[coverlet-issues]: https://github.com/coverlet-coverage/coverlet/issues/1422
[GodotSharp]: https://www.nuget.org/packages/GodotSharp/
[chickensoft-games/setup-godot]: https://github.com/chickensoft-games/setup-godot
[homebrew]: https://brew.sh/

26
addons.jsonc Normal file
View File

@@ -0,0 +1,26 @@
// Godot addons configuration file for use with the GodotEnv tool.
// See https://github.com/chickensoft-games/GodotEnv for more info.
// -------------------------------------------------------------------- //
// Note: this is a JSONC file, so you can use comments!
// If using Rider, see https://youtrack.jetbrains.com/issue/RIDER-41716
// for any issues with JSONC.
// -------------------------------------------------------------------- //
{
"$schema": "https://chickensoft.games/schemas/addons.schema.json",
// "path": "addons", // default
// "cache": ".addons", // default
"addons": {
"imrp": { // name must match the folder name in the repository
"url": "https://github.com/MakovWait/improved_resource_picker",
// "source": "remote", // default
// "checkout": "main", // default
"subfolder": "addons/imrp"
},
"godot_debug_draw_3d": {
"url": "https://github.com/DmitriySalnikov/godot_debug_draw_3d/releases/download/1.7.3/debug-draw-3d_1.7.3.zip/",
"source": "zip", // optional — this is the default
// "checkout": "master", // optional — this is the default
// "subfolder": "addons/debug_draw_3d" // optional — defaults to "/"
},
}
}

12
addons/.editorconfig Normal file
View File

@@ -0,0 +1,12 @@
# Editor configs in nested directories override those in parent directories
# for the directory in which they are placed.
#
# This editor config prevents the code editor from analyzing C# files which
# belong to addons.
#
# Ignoring C# addon scripts is generally preferable, since C# can be coded
# in a variety of ways that may or may not trigger warnings based on your
# own editorconfig or IDE settings.
[*.cs]
generated_code = true

1
assets Submodule

Submodule assets added at b5ad429c59

0
badges/.gdignore Normal file
View File

113
badges/branch_coverage.svg Normal file
View File

@@ -0,0 +1,113 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="155" height="20">
<style type="text/css">
<![CDATA[
@keyframes fade1 {
0% { visibility: visible; opacity: 1; }
27% { visibility: visible; opacity: 1; }
33% { visibility: hidden; opacity: 0; }
60% { visibility: hidden; opacity: 0; }
66% { visibility: hidden; opacity: 0; }
93% { visibility: hidden; opacity: 0; }
100% { visibility: visible; opacity: 1; }
}
@keyframes fade2 {
0% { visibility: hidden; opacity: 0; }
27% { visibility: hidden; opacity: 0; }
33% { visibility: visible; opacity: 1; }
60% { visibility: visible; opacity: 1; }
66% { visibility: hidden; opacity: 0; }
93% { visibility: hidden; opacity: 0; }
100% { visibility: hidden; opacity: 0; }
}
@keyframes fade3 {
0% { visibility: hidden; opacity: 0; }
27% { visibility: hidden; opacity: 0; }
33% { visibility: hidden; opacity: 0; }
60% { visibility: hidden; opacity: 0; }
66% { visibility: visible; opacity: 1; }
93% { visibility: visible; opacity: 1; }
100% { visibility: hidden; opacity: 0; }
}
.linecoverage {
animation-duration: 15s;
animation-name: fade1;
animation-iteration-count: infinite;
}
.branchcoverage {
animation-duration: 15s;
animation-name: fade2;
animation-iteration-count: infinite;
}
.methodcoverage {
animation-duration: 15s;
animation-name: fade3;
animation-iteration-count: infinite;
}
]]>
</style>
<title>Code coverage</title>
<defs>
<linearGradient id="gradient" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<linearGradient id="c">
<stop offset="0" stop-color="#d40000"/>
<stop offset="1" stop-color="#ff2a2a"/>
</linearGradient>
<linearGradient id="a">
<stop offset="0" stop-color="#e0e0de"/>
<stop offset="1" stop-color="#fff"/>
</linearGradient>
<linearGradient id="b">
<stop offset="0" stop-color="#37c837"/>
<stop offset="1" stop-color="#217821"/>
</linearGradient>
<linearGradient xlink:href="#a" id="e" x1="106.44" x2="69.96" y1="-11.96" y2="-46.84" gradientTransform="matrix(-.8426 -.00045 -.00045 -.8426 -94.27 -75.82)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="f" x1="56.19" x2="77.97" y1="-23.45" y2="10.62" gradientTransform="matrix(.8426 .00045 .00045 .8426 94.27 75.82)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="g" x1="79.98" x2="132.9" y1="10.79" y2="10.79" gradientTransform="matrix(.8426 .00045 .00045 .8426 94.27 75.82)" gradientUnits="userSpaceOnUse"/>
<mask id="mask">
<rect width="155" height="20" rx="3" fill="#fff"/>
</mask>
<g id="icon" transform="matrix(.04486 0 0 .04481 -.48 -.63)">
<rect width="52.92" height="52.92" x="-109.72" y="-27.13" fill="url(#e)" transform="rotate(-135)"/>
<rect width="52.92" height="52.92" x="70.19" y="-39.18" fill="url(#f)" transform="rotate(45)"/>
<rect width="52.92" height="52.92" x="80.05" y="-15.74" fill="url(#g)" transform="rotate(45)"/>
</g>
</defs>
<g mask="url(#mask)">
<rect x="0" y="0" width="90" height="20" fill="#444"/>
<rect x="90" y="0" width="20" height="20" fill="#c00"/>
<rect x="110" y="0" width="45" height="20" fill="#00B600"/>
<rect x="0" y="0" width="155" height="20" fill="url(#gradient)"/>
</g>
<g>
<path class="" fill="#fff" d="m 97.627847,15.246584 q 0,-0.36435 -0.255043,-0.619412 -0.255042,-0.254975 -0.619388,-0.254975 -0.364346,0 -0.619389,0.254975 -0.255042,0.255062 -0.255042,0.619412 0,0.36435 0.255042,0.619412 0.255043,0.254975 0.619389,0.254975 0.364346,0 0.619388,-0.254975 0.255043,-0.255062 0.255043,-0.619412 z m 0,-10.4931686 q 0,-0.3643498 -0.255043,-0.6194121 -0.255042,-0.2550624 -0.619388,-0.2550624 -0.364346,0 -0.619389,0.2550624 -0.255042,0.2550623 -0.255042,0.6194121 0,0.3643498 0.255042,0.6193246 0.255043,0.2551499 0.619389,0.2551499 0.364346,0 0.619388,-0.2551499 0.255043,-0.2549748 0.255043,-0.6193246 z m 5.829537,1.1659368 q 0,-0.3643498 -0.255042,-0.6194121 -0.255042,-0.2550624 -0.619388,-0.2550624 -0.364347,0 -0.619389,0.2550624 -0.255042,0.2550623 -0.255042,0.6194121 0,0.3643497 0.255042,0.6193246 0.255042,0.2550623 0.619389,0.2550623 0.364346,0 0.619388,-0.2550623 0.255042,-0.2549749 0.255042,-0.6193246 z m 0.874431,0 q 0,0.4736372 -0.236825,0.8789369 -0.236824,0.4052998 -0.637606,0.6330621 -0.01822,2.6142358 -2.058555,3.7709858 -0.619388,0.346149 -1.849057,0.737799 -1.165908,0.36435 -1.543916,0.646712 -0.378009,0.282363 -0.378009,0.910875 l 0,0.236862 q 0.40078,0.227675 0.637605,0.633062 0.236825,0.4053 0.236825,0.878937 0,0.7287 -0.510084,1.238824 -0.510085,0.510038 -1.238777,0.510038 -0.728692,0 -1.238777,-0.510038 -0.510085,-0.510124 -0.510085,-1.238824 0,-0.473637 0.236825,-0.878937 0.236826,-0.405387 0.637606,-0.633062 l 0,-7.469083 q -0.40078,-0.2277624 -0.637606,-0.6331496 -0.236825,-0.4052998 -0.236825,-0.878937 0,-0.7286996 0.510085,-1.2388242 0.510085,-0.5100372 1.238777,-0.5100372 0.728692,0 1.238777,0.5100372 0.510084,0.5101246 0.510084,1.2388242 0,0.4736372 -0.236825,0.878937 -0.236825,0.4053872 -0.637605,0.6331496 l 0,4.526985 q 0.491866,-0.236862 1.402732,-0.519225 0.500976,-0.154875 0.797007,-0.268712 0.296031,-0.1138373 0.64216,-0.2823623 0.346129,-0.168525 0.537411,-0.3598 0.191281,-0.191275 0.3689,-0.4645374 0.177619,-0.2732623 0.255042,-0.6330621 0.07742,-0.3597998 0.07742,-0.833437 -0.40078,-0.2277623 -0.637606,-0.6330621 -0.236824,-0.4052997 -0.236824,-0.8789369 0,-0.7286996 0.510084,-1.2388243 0.510085,-0.5101246 1.238777,-0.5101246 0.728693,0 1.238777,0.5101246 0.510084,0.5101247 0.510084,1.2388243 z"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Arial,Geneva,sans-serif" font-size="11">
<a xlink:href="https://github.com/danielpalme/ReportGenerator" target="_top">
<title>Generated by: ReportGenerator 5.1.17.0</title>
<use xlink:href="#icon" transform="translate(3,1) scale(3.5)"/>
</a>
<text x="53" y="15" fill="#010101" fill-opacity=".3">Coverage</text>
<text x="53" y="14" fill="#fff">Coverage</text>
<text class="" x="132.5" y="15" fill="#010101" fill-opacity=".3">N/A</text><text class="" x="132.5" y="14">N/A</text>
</g>
<g>
<rect class="" x="90" y="0" width="65" height="20" fill-opacity="0"><title>Branch coverage</title></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

113
badges/line_coverage.svg Normal file
View File

@@ -0,0 +1,113 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="155" height="20">
<style type="text/css">
<![CDATA[
@keyframes fade1 {
0% { visibility: visible; opacity: 1; }
27% { visibility: visible; opacity: 1; }
33% { visibility: hidden; opacity: 0; }
60% { visibility: hidden; opacity: 0; }
66% { visibility: hidden; opacity: 0; }
93% { visibility: hidden; opacity: 0; }
100% { visibility: visible; opacity: 1; }
}
@keyframes fade2 {
0% { visibility: hidden; opacity: 0; }
27% { visibility: hidden; opacity: 0; }
33% { visibility: visible; opacity: 1; }
60% { visibility: visible; opacity: 1; }
66% { visibility: hidden; opacity: 0; }
93% { visibility: hidden; opacity: 0; }
100% { visibility: hidden; opacity: 0; }
}
@keyframes fade3 {
0% { visibility: hidden; opacity: 0; }
27% { visibility: hidden; opacity: 0; }
33% { visibility: hidden; opacity: 0; }
60% { visibility: hidden; opacity: 0; }
66% { visibility: visible; opacity: 1; }
93% { visibility: visible; opacity: 1; }
100% { visibility: hidden; opacity: 0; }
}
.linecoverage {
animation-duration: 15s;
animation-name: fade1;
animation-iteration-count: infinite;
}
.branchcoverage {
animation-duration: 15s;
animation-name: fade2;
animation-iteration-count: infinite;
}
.methodcoverage {
animation-duration: 15s;
animation-name: fade3;
animation-iteration-count: infinite;
}
]]>
</style>
<title>Code coverage</title>
<defs>
<linearGradient id="gradient" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<linearGradient id="c">
<stop offset="0" stop-color="#d40000"/>
<stop offset="1" stop-color="#ff2a2a"/>
</linearGradient>
<linearGradient id="a">
<stop offset="0" stop-color="#e0e0de"/>
<stop offset="1" stop-color="#fff"/>
</linearGradient>
<linearGradient id="b">
<stop offset="0" stop-color="#37c837"/>
<stop offset="1" stop-color="#217821"/>
</linearGradient>
<linearGradient xlink:href="#a" id="e" x1="106.44" x2="69.96" y1="-11.96" y2="-46.84" gradientTransform="matrix(-.8426 -.00045 -.00045 -.8426 -94.27 -75.82)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#b" id="f" x1="56.19" x2="77.97" y1="-23.45" y2="10.62" gradientTransform="matrix(.8426 .00045 .00045 .8426 94.27 75.82)" gradientUnits="userSpaceOnUse"/>
<linearGradient xlink:href="#c" id="g" x1="79.98" x2="132.9" y1="10.79" y2="10.79" gradientTransform="matrix(.8426 .00045 .00045 .8426 94.27 75.82)" gradientUnits="userSpaceOnUse"/>
<mask id="mask">
<rect width="155" height="20" rx="3" fill="#fff"/>
</mask>
<g id="icon" transform="matrix(.04486 0 0 .04481 -.48 -.63)">
<rect width="52.92" height="52.92" x="-109.72" y="-27.13" fill="url(#e)" transform="rotate(-135)"/>
<rect width="52.92" height="52.92" x="70.19" y="-39.18" fill="url(#f)" transform="rotate(45)"/>
<rect width="52.92" height="52.92" x="80.05" y="-15.74" fill="url(#g)" transform="rotate(45)"/>
</g>
</defs>
<g mask="url(#mask)">
<rect x="0" y="0" width="90" height="20" fill="#444"/>
<rect x="90" y="0" width="20" height="20" fill="#c00"/>
<rect x="110" y="0" width="45" height="20" fill="#00B600"/>
<rect x="0" y="0" width="155" height="20" fill="url(#gradient)"/>
</g>
<g>
<path class="" stroke="#fff" d="M94 6.5 h12 M94 10.5 h12 M94 14.5 h12"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Arial,Geneva,sans-serif" font-size="11">
<a xlink:href="https://github.com/danielpalme/ReportGenerator" target="_top">
<title>Generated by: ReportGenerator 5.1.17.0</title>
<use xlink:href="#icon" transform="translate(3,1) scale(3.5)"/>
</a>
<text x="53" y="15" fill="#010101" fill-opacity=".3">Coverage</text>
<text x="53" y="14" fill="#fff">Coverage</text>
<text class="" x="132.5" y="15" fill="#010101" fill-opacity=".3">100%</text><text class="" x="132.5" y="14">100%</text>
</g>
<g>
<rect class="" x="90" y="0" width="65" height="20" fill-opacity="0"><title>Line coverage</title></rect>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

53
coverage.ps1 Normal file
View File

@@ -0,0 +1,53 @@
# To collect code coverage, you will need the following environment setup:
#
# - A "GODOT" environment variable pointing to the Godot executable
# - ReportGenerator installed
#
# dotnet tool install -g dotnet-reportgenerator-globaltool
#
# - A version of coverlet > 3.2.0.
#
# As of Jan 2023, this is not yet released.
#
# The included `nuget.config` file will allow you to install a nightly
# version of coverlet from the coverlet nightly nuget feed.
#
# dotnet tool install --global coverlet.console --prerelease.
#
# You can build coverlet yourself, but you will need to edit the path to
# coverlet below to point to your local build of the coverlet dll.
dotnet build --no-restore
coverlet `
"./.godot/mono/temp/bin/Debug" --verbosity detailed `
--target $env:GODOT `
--targetargs "--run-tests --coverage --quit-on-finish" `
--format "opencover" `
--output "./coverage/coverage.xml" `
--exclude-by-file "**/test/**/*.cs" `
--exclude-by-file "**/*Microsoft.NET.Test.Sdk.Program.cs" `
--exclude-by-file "**/Godot.SourceGenerators/**/*.cs" `
--exclude-assemblies-without-sources "missingall"
# Projects included via <ProjectReference> will be collected in code coverage.
# If you want to exclude them, replace the string below with the names of
# the assemblies to ignore. e.g.,
# $ASSEMBLIES_TO_REMOVE="-AssemblyToRemove1;-AssemblyToRemove2"
$ASSEMBLIES_TO_REMOVE=""
reportgenerator `
-reports:"./coverage/coverage.xml" `
-targetdir:"./coverage/report" `
"-assemblyfilters:$ASSEMBLIES_TO_REMOVE" `
"-classfilters:-GodotPlugins.Game.Main;-ChickenGameTest.Main" `
-reporttypes:"Html;Badges"
# Copy badges into their own folder. The badges folder should be included in
# source control so that the README.md in the root can reference the badges.
If (!(Test-Path -Path "./badges")) {
New-Item -ItemType directory -Path "./badges"
}
Move-Item "./coverage/report/badge_branchcoverage.svg" "./badges/branch_coverage.svg" -Force
Move-Item "./coverage/report/badge_linecoverage.svg" "./badges/line_coverage.svg" -Force
Invoke-Expression ("cmd /c start coverage/report/index.htm")

79
coverage.sh Normal file
View File

@@ -0,0 +1,79 @@
#!/bin/bash
# To collect code coverage, you will need the following environment setup:
#
# - A "GODOT" environment variable pointing to the Godot executable
# - ReportGenerator installed
#
# dotnet tool install -g dotnet-reportgenerator-globaltool
#
# - A version of coverlet > 3.2.0.
#
# As of Jan 2023, this is not yet released.
#
# The included `nuget.config` file will allow you to install a nightly
# version of coverlet from the coverlet nightly nuget feed.
#
# dotnet tool install --global coverlet.console --prerelease.
#
# You can build coverlet yourself, but you will need to edit the path to
# coverlet below to point to your local build of the coverlet dll.
#
# If you need help with coverage, feel free to join the Chickensoft Discord.
# https://chickensoft.games
dotnet build --no-restore
coverlet \
"./.godot/mono/temp/bin/Debug" --verbosity detailed \
--target $GODOT \
--targetargs "--run-tests --coverage --quit-on-finish" \
--format "opencover" \
--output "./coverage/coverage.xml" \
--exclude-by-file "**/test/**/*.cs" \
--exclude-by-file "**/*Microsoft.NET.Test.Sdk.Program.cs" \
--exclude-by-file "**/Godot.SourceGenerators/**/*.cs" \
--exclude-assemblies-without-sources "missingall"
# Projects included via <ProjectReference> will be collected in code coverage.
# If you want to exclude them, replace the string below with the names of
# the assemblies to ignore. e.g.,
# ASSEMBLIES_TO_REMOVE="-AssemblyToRemove1;-AssemblyToRemove2"
ASSEMBLIES_TO_REMOVE=""
reportgenerator \
-reports:"./coverage/coverage.xml" \
-targetdir:"./coverage/report" \
"-assemblyfilters:$ASSEMBLIES_TO_REMOVE" \
"-classfilters:-GodotPlugins.Game.Main;-ChickenGameTest.Main" \
-reporttypes:"Html;Badges"
# Copy badges into their own folder. The badges folder should be included in
# source control so that the README.md in the root can reference the badges.
mkdir -p ./badges
mv ./coverage/report/badge_branchcoverage.svg ./badges/branch_coverage.svg
mv ./coverage/report/badge_linecoverage.svg ./badges/line_coverage.svg
# Determine OS, open coverage accordingly.
case "$(uname -s)" in
Darwin)
echo 'Mac OS X'
open coverage/report/index.htm
;;
Linux)
echo 'Linux'
;;
CYGWIN*|MINGW32*|MSYS*|MINGW*)
echo 'MS Windows'
start coverage/report/index.htm
;;
*)
echo 'Other OS'
;;
esac

0
coverage/.gdignore Normal file
View File

103
cspell.json Normal file
View File

@@ -0,0 +1,103 @@
{
"files": [
"**/*.*"
],
"ignorePaths": [
"**/*.tscn",
"**/*.import",
"badges/**/*.*",
"coverage/**/*.*",
".godot/**/*.*",
"**/*.tres",
"**/*.uid",
"**/*.pdb",
"**/*.godot",
"**/*.dylib",
"**/*.dll",
"**/bin/**/*.*",
"**/obj/**/*.*",
"addons/**/*.*"
],
"words": [
"animatable",
"assemblyfilters",
"automerge",
"binutils",
"branchcoverage",
"camelcase",
"ccdik",
"chickenconfig",
"chickengame",
"Chickensoft",
"classfilters",
"Cmdline",
"cubemap",
"CYGWIN",
"Decompilation",
"devbuild",
"dtls",
"dylib",
"endregion",
"fabrik",
"framebuffer",
"gdshader",
"gdshaderinc",
"globaltool",
"gltf",
"godotengine",
"hmac",
"issuecomment",
"ITEMHALFSIZE",
"joypad",
"kisak",
"lcov",
"lightmap",
"lightmapper",
"lihop",
"linecoverage",
"methodcoverage",
"missingall",
"msbuild",
"MSYS",
"mult",
"multisample",
"nameof",
"netstandard",
"NOLOGO",
"nupkg",
"occluder",
"omni",
"Omnisharp",
"opencover",
"opengl",
"OPTOUT",
"paramref",
"pascalcase",
"randomizer",
"raymarch",
"renovatebot",
"reportgenerator",
"reporttypes",
"roslynator",
"Shouldly",
"spir",
"subfolders",
"targetargs",
"targetdir",
"theora",
"triplanar",
"tscn",
"tweener",
"typeof",
"typeparam",
"typeparamref",
"ulong",
"upnp",
"vorbis",
"voxel",
"vulkan",
"Wyri",
"XRIP",
"Xunit"
]
}

3
default_bus_layout.tres Normal file
View File

@@ -0,0 +1,3 @@
[gd_resource type="AudioBusLayout" format=3 uid="uid://bx2uo1td15816"]
[resource]

0
docs/.gdignore Normal file
View File

BIN
docs/publish.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
docs/renovatebot_pr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
docs/spelling_fix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/version_change.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

9
global.json Normal file
View File

@@ -0,0 +1,9 @@
{
"sdk": {
"version": "10.0.104",
"rollForward": "major"
},
"msbuild-sdks": {
"Godot.NET.Sdk": "4.6.2"
}
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

40
icon.png.import Normal file
View File

@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d43sw60ib1iq"
path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.png"
dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

6
nuget.config Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="coverlet" value="https://pkgs.dev.azure.com/tonerdo/coverlet/_packaging/coverlet-nightly/nuget/v3/index.json" />
</packageSources>
</configuration>

20
omnisharp.json Normal file
View File

@@ -0,0 +1,20 @@
{
"RoslynExtensionsOptions": {
"enableAnalyzersSupport": true,
"enableDecompilationSupport": true,
"enableImportCompletion": true
},
"FormattingOptions": {
"enableEditorConfigSupport": true,
"organizeImports": true,
"SeparateImportDirectiveGroups": true
},
"RenameOptions": {
"renameInComments": true,
"renameOverloads": true,
"renameInStrings": true
},
"SDK": {
"includePrereleases": true
}
}

54
project.godot Normal file
View File

@@ -0,0 +1,54 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[animation]
compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="ChickenGameTest"
run/main_scene="res://src/Main.tscn"
config/features=PackedStringArray("4.6", "C#", "Mobile")
config/icon="res://icon.png"
[debug_draw_3d]
settings/addon_root_folder="res://addons/debug_draw_3d"
[display]
window/size/viewport_width=720
window/size/viewport_height=720
window/size/initial_position_type=3
window/size/window_width_override=1280
window/size/window_height_override=720
[dotnet]
project/assembly_name="ChickenGameTest"
[editor]
naming/scene_name_casing=1
[editor_plugins]
enabled=PackedStringArray("res://addons/imrp/plugin.cfg")
[gui]
theme/default_font_multichannel_signed_distance_field=true
theme/default_font_generate_mipmaps=true
theme/default_theme_scale=2.0
[rendering]
renderer/rendering_method="mobile"

4
renovate.json Normal file
View File

@@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>chickensoft-games/renovate:godot"]
}

View File

@@ -0,0 +1,71 @@
using ChickenGameTest;
using Godot;
using System;
public partial class ConveyorBeltStraight : Node3D
{
[Export] public Vector3I VoxelPos { get; set; } = default;
[Export] public int Width { get; set; } = default;
public GridTransform3D VoxelTransform
{
get => GridTransform3D.FromGodot(Transform);
set => Transform = value.ToGodot();
}
public override void _Ready()
{
var node = GetNode("Node") as TestItemConveyor;
base._Ready();
// GD.PrintS(VoxelTransform.Origin,VoxelTransform.Forward);
node.StartPort = node.CreatePort(
new(VoxelTransform.TranslatedLocal(new(1-Width,0,0)).RotateY(2),
// Direction.Back,
Width,
PortAccess.BiDirectional),
new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.Start),
new LaneSpan(0, 1));
// GD.Print(VoxelTransform.Origin,GridTransform3D.FromGodot(Transform.TranslatedLocal(Vector3.Left)).Origin);
node.EndPort = node.CreatePort(
new(VoxelTransform, //new(GridTransform3D.FromGodot(Transform.TranslatedLocal(Vector3.Left)),
// Direction.Front,
Width,
PortAccess.BiDirectional),
new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.End),
new LaneSpan(0, 1));
// node.OtherPorts = [
// node.CreatePort(
// new(VoxelTransform.RotateY90CW(),
// // Direction.Right,
// 1,
// PortAccess.In),
// new ItemConveyor.BeltTOffset(node.Length/2),
// LaneSpan.One),
// node.CreatePort(
// new(VoxelTransform.RotateY90CCW(),
// // Direction.Left,
// 1,
// PortAccess.In),
// new ItemConveyor.BeltTOffset(node.Length/2),
// LaneSpan.One)
// ];
// new ConveyorPort(this,
// new BeltPortProfile(VoxelPos, Direction.Back, 1, PortAccess.BiDirectional),
// new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.Start),
// (item, offset) =>
// {
// var obstacle = node.GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, 0, offset, LaneSpan.One);
// return ItemConveyor.ITEMSIZE < obstacle.DistanceToCenter - (obstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// },
// (item, offset) => { _items.Insert(0, new(item, offset)); return true;});
// node.EndPort = new ConveyorPort(this,
// new BeltPortProfile(VoxelPos, Direction.Front, 1, PortAccess.BiDirectional),
// new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.End),
// (item, offset) =>
// {
// var obstacle = node.GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, node.Length, offset, LaneSpan.One);
// return ItemConveyor.ITEMSIZE < obstacle.DistanceToCenter - (obstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// // return ItemConveyor.ITEMSIZE < GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardStart, Length, SpeedMagnitude, LaneSpan.One);
// },
// (item, offset) => { _items.Add(new(item, offset)); return true;});
}
}

View File

@@ -0,0 +1 @@
uid://dgyuh6un1qoj4

View File

@@ -0,0 +1,34 @@
[gd_scene load_steps=7 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="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"]
[sub_resource type="Curve3D" id="Curve3D_x5qdr"]
_data = {
"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 0, 0, 0, 0, 0, 0, 0, 0.5, -0.5),
"tilts": PackedFloat32Array(0, 0)
}
point_count = 2
[node name="ConveyorBeltStraight" type="Node3D"]
script = ExtResource("1_mn8ro")
[node name="Node" type="Node" parent="."]
script = ExtResource("2_1vr1d")
[node name="conveyor-sides" parent="." instance=ExtResource("2_mn8ro")]
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0, 0)
[node name="arrow-basic2" parent="." instance=ExtResource("3_1vr1d")]
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, 0.21879572, 0)
[node name="Path3D" type="Path3D" parent="."]
curve = SubResource("Curve3D_x5qdr")
[node name="Node2" type="Node" parent="." node_paths=PackedStringArray("Path3D", "ItemConveyor")]
script = ExtResource("4_g7n1f")
Path3D = NodePath("../Path3D")
ItemConveyor = NodePath("../Node")

View File

@@ -0,0 +1,133 @@
namespace ChickenGameTest;
using System;
using System.Collections.Generic;
// public partial class StructuralInstance<TAttribute> where TAttribute : class
// {
public sealed class StructuralBuilder<TAttribute> where TAttribute : class
{
private readonly Dictionary<int, TAttribute> _components = [];
public StructuralBuilder() { }
public StructuralBuilder(IStructuralInstance<TAttribute> existing)
{
foreach (var (id, value) in existing.GetAttributes())
{
_components[id] = value;
}
// for (int i = 0; i < existing.ComponentCount; i++)
// {
// var typeId = existing._attributesTypeIds[i];
// var component = existing.GetComponentAt(i);
// _components[typeId] = component;
// }
}
public StructuralBuilder<TAttribute> Add<T>(T component) where T : TAttribute
{
int id = ComponentTypeRegistry.GetId<T>();
_components[id] = component;
return this;
}
public StructuralBuilder<TAttribute> Upsert<T>(Func<T, T> ifExists, Func<T> none) where T : TAttribute
{
int id = ComponentTypeRegistry.GetId<T>();
_components[id] = _components.TryGetValue(id, out var old) ? ifExists((T)old) : none();
return this;
}
public StructuralBuilder<TAttribute> CombineWith(IStructuralInstance<TAttribute> other, Action<CombineBinder>? configure = null)
{
var binder = new CombineBinder();
configure?.Invoke(binder);
foreach (var (id, value) in other.GetAttributes())
{
if (binder._ignores.Contains(id))
{
continue;
}
if (_components.TryGetValue(id, out var existing)
&& binder._rules.TryGetValue(id, out var rule))
{
_components[id] = rule.Invoke(existing, value);
continue;
}
_components[id] = value;
}
return this;
}
public StructuralBuilder<TAttribute> Remove<T>() where T : TAttribute
{
int id = ComponentTypeRegistry.GetId<T>();
_components.Remove(id);
return this;
}
public bool Has<T>() where T : TAttribute
{
int id = ComponentTypeRegistry.GetId<T>();
return _components.ContainsKey(id);
}
public StructuralInstance<TAttribute> Build()
{
int count = _components.Count;
var typeIds = new int[count];
var components = new TAttribute[count];
int i = 0;
foreach (var kv in _components)
{
typeIds[i] = kv.Key;
components[i] = kv.Value;
i++;
}
Array.Sort(typeIds, components);
return new StructuralInstance<TAttribute>(typeIds, components);
}
public MutableStructuralInstance<TAttribute> BuildMutable()
{
// int count = _components.Count;
var sortedList = new SortedList<int, TAttribute>(_components);
// var typeIds = new int[count];
// var components = new TAttribute[count];
// int i = 0;
// foreach (var kv in _components)
// {
// typeIds[i] = kv.Key;
// components[i] = kv.Value;
// i++;
// }
// Array.Sort(typeIds, components);
return new MutableStructuralInstance<TAttribute>(sortedList);
}
public class CombineBinder
{
internal readonly Dictionary<int, Func<TAttribute, TAttribute, TAttribute>> _rules = [];
internal readonly HashSet<int> _ignores = [];
public CombineBinder Bind<T>(Func<T, T, T> func) where T : TAttribute
{
_rules[ComponentType<T>.Id] = (a, b) => func((T)a, (T)b);
return this;
}
public CombineBinder Ignore<T>() where T : TAttribute
{
_ignores.Add(ComponentType<T>.Id);
return this;
}
}
}
// }

View File

@@ -0,0 +1 @@
uid://bk721rnhegl0x

View File

@@ -0,0 +1,255 @@
namespace ChickenGameTest;
using System;
using System.Collections.Generic;
using System.Linq;
using Chickensoft.Sync.Primitives;
using SJK.Functional;
public interface IStructuralInstance<TAttribute> : IEquatable<IStructuralInstance<TAttribute>>
{
int ComponentCount { get; }
bool Has<T>() where T : TAttribute;
Option<T> Get<T>() where T : TAttribute;
// TAttribute GetComponentAt(int index);
IEnumerable<(int Id, TAttribute Value)> GetAttributes();
bool IEquatable<IStructuralInstance<TAttribute>>.Equals(IStructuralInstance<TAttribute>? other) => Enumerable.SequenceEqual(GetAttributes(), other.GetAttributes(), EqualityComparer<(int, TAttribute)>.Default);
int ComputeHashCode()
{
HashCode hash = new HashCode();
foreach (var item in GetAttributes())
{
hash.Add(item);
}
return hash.ToHashCode();
}
}
public sealed partial class StructuralInstance<TAttribute> : IStructuralInstance<TAttribute>, IEquatable<StructuralInstance<TAttribute>> where TAttribute : class
{
private readonly int _hashCode;
internal readonly int[] _attributesTypeIds;
internal readonly TAttribute[] _attributes;
public int ComponentCount => _attributes.Length;
public TAttribute GetComponentAt(int index) => _attributes[index];
internal StructuralInstance(int[] componentTypeIds, TAttribute[] attributes)
{
_attributesTypeIds = componentTypeIds;
_attributes = attributes;
_hashCode = (this as IStructuralInstance<TAttribute>).ComputeHashCode();
}
public bool Has<T>() where T : TAttribute
{
// int typeId = ComponentTypeRegistry.GetId<T>();
var typeId = ComponentType<T>.Id;
return Array.BinarySearch(_attributesTypeIds, typeId) >= 0;
}
public Option<T> Get<T>() where T : TAttribute
{
// int typeId = ComponentTypeRegistry.GetId<T>();
var typeId = ComponentType<T>.Id;
int index = Array.BinarySearch(_attributesTypeIds, typeId);
if (index < 0)
{
return Option<T>.None;
}
return Option<T>.Some((T)_attributes[index]);
}
public bool Equals(StructuralInstance<TAttribute>? other)
{
if (ReferenceEquals(this, other))
{
return true;
}
if (other is null)
{
return false;
}
return _hashCode == other._hashCode && Enumerable.SequenceEqual(_attributes, other._attributes, EqualityComparer<TAttribute>.Default);// && AttributesAreEqual(other);
}
public override bool Equals(object? obj) => obj is StructuralInstance<TAttribute> value? Equals(value) : obj is IStructuralInstance<TAttribute> v && v.Equals(this);
public override int GetHashCode() => _hashCode;
public IEnumerable<(int Id, TAttribute Value)> GetAttributes()
{
for (int i = 0; i < ComponentCount; i++)
{
yield return (_attributesTypeIds[i], _attributes[i]);
}
}
public override string ToString() => base.ToString();
}
public sealed class MutableStructuralInstance<TAttribute> : IStructuralInstance<TAttribute>, IEquatable<MutableStructuralInstance<TAttribute>> where TAttribute : class
{
private readonly SortedList<int, TAttribute> _attributes = [];
public int ComponentCount => _attributes.Count;
public MutableStructuralInstance(SortedList<int, TAttribute> attributes)
{
_attributes = attributes;
}
public Option<T> Get<T>() where T : TAttribute => _attributes.TryGetValue(ComponentType<T>.Id, out var attribute) ? Option<T>.Some((T)attribute) : Option<T>.None;
public IEnumerable<(int Id, TAttribute Value)> GetAttributes() => _attributes.Select(x => (x.Key, x.Value));
public TAttribute GetComponentAt(int index) => _attributes[index];
public bool Has<T>() where T : TAttribute => _attributes.ContainsKey(ComponentType<T>.Id);
public void Set<T>(T value) where T : TAttribute => _attributes[ComponentType<T>.Id] = value;
public override int GetHashCode() => (this as IStructuralInstance<TAttribute>).ComputeHashCode();
public bool Equals(MutableStructuralInstance<TAttribute>? other) => other is not null && Enumerable.SequenceEqual(_attributes, other._attributes, EqualityComparer<KeyValuePair<int, TAttribute>>.Default);
public override bool Equals(object? obj) => obj is MutableStructuralInstance<TAttribute> other ? Equals(other) : obj is IStructuralInstance<TAttribute> v && v.Equals(this);
}
public static class ComponentTypeRegistry
{
private static readonly Dictionary<Type, int> _typeToId = [];
private static readonly List<Type> _idToType = [];
public static int GetId(Type type)
{
if (_typeToId.TryGetValue(type, out var id))
{
return id;
}
id = _idToType.Count;
_typeToId[type] = id;
_idToType.Add(type);
return id;
}
public static int GetId<T>() => GetId(typeof(T));
}
public static class ComponentType<T>
{
public static readonly int Id = ComponentTypeRegistry.GetId<T>();
public static readonly bool CacheTransitions = Resolve();
private static bool Resolve()
{
var attr = typeof(T).GetCustomAttributes(typeof(ComponentOptionsAttribute), false);
return attr.OfType<ComponentOptionsAttribute>().FirstOrNone().Map(static f => f.CacheTransitions).Or(true);
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class ComponentOptionsAttribute : Attribute
{
public bool CacheTransitions { get; init; } = true;
}
public interface IStructureRegistry<TAttribute> where TAttribute : class
{
StructuralInstance<TAttribute> Canonicalize(StructuralInstance<TAttribute> value);
StructuralInstance<TAttribute> AddOrReplaceAttribute<T>(StructuralInstance<TAttribute> instance, T attribute) where T : TAttribute;
StructuralInstance<TAttribute> RemoveAttribute<T>(StructuralInstance<TAttribute> instance) where T : TAttribute;
}
public class StructuralInstanceManger<TAttribute> : IStructureRegistry<TAttribute> where TAttribute : class
{
private readonly HashSet<StructuralInstance<TAttribute>> _instances = [];
public StructuralInstance<TAttribute> Add<T>(StructuralInstance<TAttribute> instance, T component) where T : TAttribute
{
var builder = new StructuralBuilder<TAttribute>(instance).Add(component);
return Canonicalize(builder.Build());
}
public StructuralInstance<TAttribute> Canonicalize(StructuralInstance<TAttribute> value)
{
if (_instances.TryGetValue(value, out var id))
{
return id;
}
_instances.Add(value);
return value;
}
public StructuralInstance<TAttribute> AddOrReplaceAttribute<T>(StructuralInstance<TAttribute> instance, T attribute) where T : TAttribute
{
if (!graph.TryGetValue(instance, out var results))
{
graph[instance] = results = [];
}
for (int i = 0; i < results.Count; i++)
{
if (results[i] is AddTransitionEntry<T> addTransitionEntry && EqualityComparer<T>.Default.Equals(addTransitionEntry.Value, attribute))
{
return addTransitionEntry.Result;
}
}
var result =new AddTransitionEntry<T>(ComponentType<T>.Id, attribute, Canonicalize(new StructuralBuilder<TAttribute>(instance).Add(attribute).Build()));
if (ComponentType<T>.CacheTransitions)
{
results.Add(result);
}
return result.Result;
}
public StructuralInstance<TAttribute> RemoveAttribute<T>(StructuralInstance<TAttribute> instance) where T : TAttribute
{
if (!graph.TryGetValue(instance, out var results))
{
graph[instance] = results = [];
}
var id = ComponentType<T>.Id;
for (int i = 0; i < results.Count; i++)
{
if (results[i] is RemoveTransitionEntry removeTransitionEntry && removeTransitionEntry.Id == id)
{
return removeTransitionEntry.Result;
}
}
var result =new RemoveTransitionEntry(id, Canonicalize(new StructuralBuilder<TAttribute>(instance).Remove<T>().Build()));
if (ComponentType<T>.CacheTransitions)
{
results.Add(result);
}
return result.Result;
}
private Dictionary<StructuralInstance<TAttribute>, List<TransitionEntry>> graph = [];
private record TransitionEntry(int Id);
private record AddTransitionEntry<T>(int Id, T Value, StructuralInstance<TAttribute> Result) : TransitionEntry(Id) where T : TAttribute;
private record RemoveTransitionEntry(int Id, StructuralInstance<TAttribute> Result) : TransitionEntry(Id);
// private record ModifyTransitionEntry<T>(int Id, TAttribute Value, StructuralInstance<TAttribute> Result) : TransitionEntry(Id) where T : TAttribute;
}
public class AutoStructureInstance<TAttribute> where TAttribute : class
{
private StructuralInstance<TAttribute> _structuralInstance;
private IStructureRegistry<TAttribute> _registry;
private Dictionary<int,List<Delegate>> _bindings = [];
public void Set<T>(T? value) where T : TAttribute
{
var old = _structuralInstance.Get<T>();
if (old.HasValue && old.Value.Equals(value))
{
return;
}
if (value is null)
{
_structuralInstance = _registry.RemoveAttribute<T>(_structuralInstance);
}
else
{
_structuralInstance = _registry.AddOrReplaceAttribute(_structuralInstance, value);
}
_bindings[ComponentType<T>.Id].ForEach(item => item.DynamicInvoke(old,value));
}
public void Bind<T>(Action<T?, T?> callback) where T : TAttribute
{
int id = ComponentType<T>.Id;
if (!_bindings.TryGetValue(id, out var list))
{
_bindings[id] = list = [];
}
list.Add(callback);
}
}

View File

@@ -0,0 +1 @@
uid://cexmjk01dgtbe

View File

@@ -0,0 +1,69 @@
namespace ChickenGameTest;
using System.Diagnostics;
using Godot;
using SJK.Functional;
public class TestStructural
{
public static void Test()
{
var a = new StructuralBuilder<object>()
.Add(new Vector3(0,0,0))
.Add(new Vector2(0,5))
.Add(new Vector4(0,0,0,0))
.Add(new Aabb())
.Add(new int())
.Add(new float())
.Add(new Node())
.Add(new Node())
.Add(new Node2D())
.Add("")
.Add(new test(){a = 5, b = "gg"})
.Build();
var registy = new StructuralInstanceManger<object>();
var b = registy.Add(a,new Vector2(1,2));
var c = registy.Add(a,new Vector2(1,2));
var d = registy.Canonicalize(new StructuralBuilder<object>(a).Add(new Vector2(1,2)).Build());
var g = new StructuralBuilder<object>(a).Add(Option<int>.Some(5)).Build();
var h = new StructuralBuilder<object>(a).Add(Option<int>.Some(5)).BuildMutable();
GD.Print(g.Equals(h));
GD.Print(ReferenceEquals(b,c));
GD.Print(ReferenceEquals(b,d));
GD.Print(ComponentType<Vector3>.Id);
GD.Print(ComponentType<Vector2>.Id);
var help = new StructuralBuilder<object>(a)
.CombineWith(b, static configure => configure
.Bind<Vector2>(static (a, b) => a + b)
.Ignore<Vector4>())
.Add(Vector2I.Zero)
.Build();
GD.Print(help.Get<Vector2>().Map(i=>$"{i}").OrDefault("none"));
var timer = new Stopwatch();
timer.Start();
for (int i = 0; i < 100000; i++)
{
var e = registy.AddOrReplaceAttribute(a,new test());
}
timer.Stop();
GD.Print(timer.Elapsed);
timer.Reset();
timer.Start();
for (int i = 0; i < 100000; i++)
{
var e = registy.Canonicalize(new StructuralBuilder<object>(a).Add(new test()).Build());
}
timer.Stop();
GD.Print(timer.Elapsed);
}
}
[ComponentOptions(CacheTransitions = true)]
record test
{
public int a;
public string b;
}

View File

@@ -0,0 +1 @@
uid://br6rfqtryq0cx

15
src/Game.cs Normal file
View File

@@ -0,0 +1,15 @@
namespace ChickenGameTest;
using Godot;
public partial class Game : Control
{
public Button TestButton { get; private set; } = default!;
public int ButtonPresses { get; private set; }
public override void _Ready()
=> TestButton = GetNode<Button>("%TestButton");
public void OnTestButtonPressed() => ButtonPresses++;
}

1
src/Game.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://bcadf3uhcfy2

30
src/Game.tscn Normal file
View File

@@ -0,0 +1,30 @@
[gd_scene load_steps=2 format=3 uid="uid://cywpu6lxdjhuu"]
[ext_resource type="Script" path="res://src/Game.cs" id="1_17mmo"]
[node name="Control" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_17mmo")
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="CenterContainer"]
layout_mode = 2
[node name="TestButton" type="Button" parent="CenterContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Test Button"
[connection signal="pressed" from="CenterContainer/VBoxContainer/TestButton" to="." method="OnTestButtonPressed"]

268
src/Items/Item.cs Normal file
View File

@@ -0,0 +1,268 @@
namespace ChickenGameTest;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Godot;
/// <summary>
/// Items that can be on a belt
/// </summary>
public interface IBeltItem : IDisposable
{
int Width { get; }
int Height { get; }
Node3D CreateItemVisual();
delegate void ItemRemoved(IBeltItem item);
event ItemRemoved Disposed;
}
public class TestItem() : IBeltItem
{
public float Temp {get;set;}
public int Width {get;set;}
public int Height {get;set;}
public Node3D CreateItemVisual() => new MeshInstance3D(){Mesh = new BoxMesh(){Size = new(.1f,.1f,.1f)}};
bool _disposed;
public event IBeltItem.ItemRemoved Disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
Disposed?.Invoke(this);
}
}
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;
}
public interface IBeltPort
{
BeltPortProfile Profile { get; }
bool CanAccept(
IBeltItem item,
LaneSpan laneSpan,
float beltT
);
bool TryInsert(
IBeltItem item,
LaneSpan laneSpan,
float beltT
);
IEnumerable<Vector3I> Points()
{
for (int i = 0; i < Profile.Width; i++)
{
yield return (Profile.LocalOffset.Right * i) + Profile.LocalOffset.Origin;
}
}
LaneSpan LaneSpan => new LaneSpan(0,(ushort)Profile.Width);
}
public sealed class ConveyorPort : IBeltPort
{
public BeltPortProfile Profile { get; }
public IMovementConveyor Conveyor { get; }
public ItemConveyor.BeltT BeltT { get; }
private readonly Func<IBeltItem, float, LaneSpan, bool>? _canAccept;
private readonly Func<IBeltItem, float, LaneSpan, bool>? _accept;
public ConveyorPort(
IMovementConveyor conveyor,
BeltPortProfile profile,
ItemConveyor.BeltT beltT,
Func<IBeltItem, float, LaneSpan, bool>? canAccept,
Func<IBeltItem, float, LaneSpan, bool>? accept)
{
Conveyor = conveyor;
Profile = profile;
BeltT = beltT;
_canAccept = canAccept;
_accept = accept;
}
public bool CanAccept(
IBeltItem item,
LaneSpan laneSpan,
float beltT)
{
if (!Profile.Access.HasFlag(PortAccess.In))
{
return false;
}
if (_canAccept is null || _accept is null)
{
return false;
}
// LaneSpan checks live here, not in delegates
if (!LaneSpan.Encapsulates(
new LaneSpan(0, (ushort)Profile.Width),
laneSpan))
{
return false;
}
return _canAccept(item, beltT, laneSpan);
}
public bool TryInsert(
IBeltItem item,
LaneSpan laneSpan,
float beltT)
{
if (!CanAccept(item, laneSpan, beltT))
{
return false;
}
return _accept!(item, beltT, laneSpan);
}
}
public interface IBeltSlotProfile
{
Vector3I Position { get; }
// Direction Direction { get; }
int Width { get; }
PortAccess Access { get; }
// bool CanAcceptItem(IBeltItem beltItem, LaneSpan laneSpan, float beltT = 0);
// /// <summary>
// ///Tries to insert an belt item offset by laneSpan,
// /// </summary>
// /// <param name="beltItem"></param>
// /// <param name="laneSpan"></param>
// /// <param name="beltT"></param>
// /// <returns></returns>
// bool TryInsertItem(IBeltItem beltItem, LaneSpan laneSpan, float beltT = 0);
}
public record LaneId(int Index);
[Flags]
public enum PortAccess : byte
{
// Disabled = 0,
In = 1,
Out = 2,
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 SlotExtesion
{
public static LaneSpan MapLaneSpanToFacingPort(this IBeltPort self, IBeltPort other) => MapSlotToFacingSlot(self.Profile.LocalOffset, self.Profile.Width, other.Profile.LocalOffset, other.Profile.Width);
public static LaneSpan MapSlotToFacingSlot(
GridTransform3D fromTx,
int fromWidth,
GridTransform3D toTx,
int toWidth)
{
ushort minLane = ushort.MaxValue;
ushort maxLane = ushort.MinValue;
// In port-local space:
// +Z = forward
// +X = width axis
// origin = port center / lane 0 start
// (adjust if your convention differs)
for (int i = 0; i < fromWidth; i++)
{
var fromLocalLane = new Vector3I(i, 0, 0);
var fromLocalFacing = fromLocalLane + Vector3I.Forward;
var worldFacing = fromTx.LocalToWorld(fromLocalFacing);
var toLocal = toTx.WorldToLocal(worldFacing);
int laneIndex = toLocal.X;
bool inWidth = laneIndex >= 0 && laneIndex < toWidth;
bool onFacePlane = toLocal.Z == 0; // directly entering target face
if (!inWidth || !onFacePlane)
continue;
minLane = (ushort)Math.Min(minLane, laneIndex);
maxLane = (ushort)Math.Max(maxLane, laneIndex);
}
if (minLane > maxLane)
return LaneSpan.Zero;
return new LaneSpan(minLane, (ushort)(maxLane + 1));
}
// Map a sub-span from 'from' port into the 'to' port space
public static LaneSpan MapLaneSpan(this IBeltPort from, IBeltPort to, LaneSpan incoming)
{
ushort minLane = ushort.MaxValue;
ushort maxLane = ushort.MinValue;
// Loop over each lane in the incoming span
for (int i = incoming.Start; i < incoming.End; i++)
{
// from-local lane
var fromLocalLane = new Vector3I(i, 0, 0);
// shift forward along +Z to "face" the target
var fromLocalFacing = fromLocalLane + Vector3I.Forward;
// world space
var worldFacing = from.Profile.LocalOffset.LocalToWorld(fromLocalFacing);
// target local space
var toLocal = to.Profile.LocalOffset.WorldToLocal(worldFacing);
int laneIndex = toLocal.X;
bool inWidth = laneIndex >= 0 && laneIndex < to.Profile.Width;
bool onFacePlane = toLocal.Z == 0; // entering target face
if (!inWidth || !onFacePlane)
continue;
minLane = (ushort)Math.Min(minLane, laneIndex);
maxLane = (ushort)Math.Max(maxLane, laneIndex);
}
if (minLane > maxLane)
return LaneSpan.Zero;
return new LaneSpan(minLane, (ushort)(maxLane + 1));
}
}

1
src/Items/Item.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://dacksxt66lea4

52
src/Main.cs Normal file
View File

@@ -0,0 +1,52 @@
namespace ChickenGameTest;
using Godot;
using Chickensoft.GameTools.Displays;
#if RUN_TESTS
using System.Reflection;
using Chickensoft.GoDotTest;
#endif
// This entry-point file is responsible for determining if we should run tests.
//
// If you want to edit your game's main entry-point, please see Game.tscn and
// Game.cs instead.
public partial class Main : Node2D
{
public Vector2I DesignResolution => Display.UHD4k;
#if RUN_TESTS
public TestEnvironment Environment = default!;
#endif
public override void _Ready()
{
// Correct any erroneous scaling and guess sensible defaults.
GetWindow().LookGood(WindowScaleBehavior.UIFixed, DesignResolution);
#if RUN_TESTS
// If this is a debug build, use GoDotTest to examine the
// command line arguments and determine if we should run tests.
Environment = TestEnvironment.From(OS.GetCmdlineArgs());
// Environment = new TestEnvironment(true, false, true, false, false, false, null, OS.GetCmdlineArgs());
GD.Print(Environment.ShouldRunTests);
if (Environment.ShouldRunTests)
{
CallDeferred("RunTests");
return;
}
#endif
// If we don't need to run tests, we can just switch to the game scene.
CallDeferred("RunScene");
}
#if RUN_TESTS
private void RunTests()
=> _ = GoTest.RunTests(Assembly.GetExecutingAssembly(), this, Environment);
#endif
private void RunScene()
=> GetTree().ChangeSceneToFile("res://src/Game.tscn");
}

1
src/Main.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://d0cxxx3axr1j

6
src/Main.tscn Normal file
View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://du7hlwjl6vw6l"]
[ext_resource type="Script" path="res://src/Main.cs" id="1_prpoe"]
[node name="Node2D" type="Node2D"]
script = ExtResource("1_prpoe")

222
src/Math/GridTransform3D.cs Normal file
View File

@@ -0,0 +1,222 @@
using Godot;
using System;
public readonly struct GridTransform3D : IEquatable<GridTransform3D>
{
public readonly Vector3I Origin;
// Basis vectors (columns of a rotation matrix)
public readonly Vector3I Right; // local +X
public readonly Vector3I Up; // local +Y
public readonly Vector3I Forward; // local +Z
#region Constructors
public GridTransform3D(
Vector3I origin,
Vector3I right,
Vector3I up,
Vector3I forward)
{
Origin = origin;
Right = right;
Up = up;
Forward = forward;
}
public static GridTransform3D Identity =>
new(
Vector3I.Zero,
Vector3I.Right,
Vector3I.Up,
Vector3I.Forward
);
public static GridTransform3D At(Vector3I origin) =>
new(origin, Vector3I.Right, Vector3I.Up, Vector3I.Forward);
#endregion
#region Transform ops
/// Transform a local point into world space
public Vector3I TransformPoint(Vector3I local)
{
return Origin
+ (Right * local.X)
+ (Up * local.Y)
+ (Forward * local.Z);
}
/// Transform a direction (ignores translation)
public Vector3I TransformDirection(Vector3I dir)
{
return (Right * dir.X)
+ (Up * dir.Y)
+ (Forward * dir.Z);
}
public Vector3I LocalToWorld(Vector3I local)
{
return Origin
+ Right * local.X
+ Up * local.Y
+ Forward * local.Z;
}
public Vector3I WorldToLocal(Vector3I world)
{
Vector3I delta = world - Origin;
return new Vector3I(
Dot(delta, Right),
Dot(delta, Up),
Dot(delta, Forward)
);
}
private static int Dot(Vector3I a, Vector3I b)
=> a.X * b.X + a.Y * b.Y + a.Z * b.Z;
public readonly GridTransform3D Translated(Vector3I offset)
{
return new GridTransform3D( Origin + offset,Right,Up,Forward);
}
public readonly GridTransform3D TranslatedLocal(Vector3I offset)
{
return new GridTransform3D( new Vector3I(Origin.X + Dot(Right,offset), Origin.Y+ Dot(Up,offset), Origin.Z + Dot(Forward,offset)),Right,Up,Forward);
}
#endregion
#region Composition
/// Compose two transforms (this * other)
public GridTransform3D Multiply(GridTransform3D other)
{
return new GridTransform3D(
TransformPoint(other.Origin),
TransformDirection(other.Right),
TransformDirection(other.Up),
TransformDirection(other.Forward)
);
}
public static GridTransform3D operator *(GridTransform3D a, GridTransform3D b)
=> a.Multiply(b);
#endregion
#region Rotations (90° steps)
public GridTransform3D RotateY90CW()
{
// Y stays, X -> Z, Z -> -X
return new GridTransform3D(
Origin,
Forward,
Up,
-Right
);
}
public GridTransform3D RotateY90CCW()
{
// Y stays, X -> -Z, Z -> X
return new GridTransform3D(
Origin,
-Forward,
Up,
Right
);
}
public GridTransform3D RotateX90CW()
{
// X stays, Y -> Z, Z -> -Y
return new GridTransform3D(
Origin,
Right,
Forward,
-Up
);
}
public GridTransform3D RotateZ90CW()
{
// Z stays, X -> Y, Y -> -X
return new GridTransform3D(
Origin,
Up,
-Right,
Forward
);
}
public GridTransform3D RotateY(int quarterTurns)
{
quarterTurns = ((quarterTurns % 4) + 4) % 4;
var t = this;
for (int i = 0; i < quarterTurns; i++)
t = t.RotateY90CW();
return t;
}
#endregion
#region Conversion to/from Godot
public Transform3D ToGodot()
{
var basis = new Basis(
(Vector3)Right,
(Vector3)Up,
(Vector3)Forward
);
return new Transform3D(basis, (Vector3)Origin);
}
public static GridTransform3D FromGodot(Transform3D t)
{
Vector3I RoundAxis(Vector3 v)
{
var a = v.Abs();
if (a.X > a.Y && a.X > a.Z)
return new Vector3I(Math.Sign(v.X), 0, 0);
if (a.Y > a.Z)
return new Vector3I(0, Math.Sign(v.Y), 0);
return new Vector3I(0, 0, Math.Sign(v.Z));
}
return new GridTransform3D(
new Vector3I(
Mathf.RoundToInt(t.Origin.X),
Mathf.RoundToInt(t.Origin.Y),
Mathf.RoundToInt(t.Origin.Z)
),
RoundAxis(t.Basis.X),
RoundAxis(t.Basis.Y),
RoundAxis(t.Basis.Z)
);
}
#endregion
#region Equality
public bool Equals(GridTransform3D other)
{
return Origin == other.Origin
&& Right == other.Right
&& Up == other.Up
&& Forward == other.Forward;
}
public override bool Equals(object? obj)
=> obj is GridTransform3D g && Equals(g);
public override int GetHashCode()
=> HashCode.Combine(Origin, Right, Up, Forward);
#endregion
}

View File

@@ -0,0 +1 @@
uid://dpskwnyblxtf4

View File

@@ -0,0 +1,23 @@
using Godot;
public static partial class SJKMath
{
// public static Vector3I Lerp(Vector3I from, Vector3I to, float weight) => new Vector3I(
// Mathf.Lerp(from.X, to.X, weight),
// Mathf.Lerp(from.Y, to.Y, weight),
// Mathf.Lerp(from.Z, to.Z, weight)
// );
public static Vector3 Lerp(Vector3 from, Vector3 to, float weight) => new Vector3(
Mathf.Lerp(from.X, to.X, weight),
Mathf.Lerp(from.Y, to.Y, weight),
Mathf.Lerp(from.Z, to.Z, weight)
);
// public static Vector2I Lerp(Vector2I from, Vector2I to, float weight) => new Vector2I(
// Mathf.Lerp(from.X, to.X, weight),
// Mathf.Lerp(from.Y, to.Y, weight)
// );
public static Vector2 Lerp(Vector2 from, Vector2 to, float weight) => new Vector2(
Mathf.Lerp(from.X, to.X, weight),
Mathf.Lerp(from.Y, to.Y, weight)
);
}

View File

@@ -0,0 +1 @@
uid://133eop5e4mii

View File

@@ -0,0 +1,42 @@
using System;
using System.Linq;
using Godot;
namespace SJK.GodotHelpers;
public static class MyNodeExtensions{
public static void FreeDeferred(this Node node)=>node.CallDeferred(Node.MethodName.Free);
/// <summary>
/// Checks if given Property exists on <c>Base</c>
/// </summary>
/// <param name="Base">Current <c>GodotObject</c></param>
/// <param name="PropertyName">Property Name</param>
/// <returns><c>bool</c> true if given property exists on Base</returns>
public static bool HasProperty(this GodotObject Base, string PropertyName){
/*
Returns the object's property list as an Godot.Collections.Array of dictionaries.
Each Godot.Collections.Dictionary contains the following entries:
- name is the property's name, as a string;
- class_name is an empty StringName, unless the property is Variant.Type.Object and it inherits from a class;
- type is the property's type, as an int (see Variant.Type);
- hint is how the property is meant to be edited (see PropertyHint);
- hint_string depends on the hint (see PropertyHint);
- usage is a combination of PropertyUsageFlags.
*/
foreach (var Property in Base.GetPropertyListEx())
{
if(Property.Name == PropertyName){
return true;
}
}
return false;
}
public static (Node node, Resource resource, NodePath remaining) GetNodeAndResourceEx(this Node self, NodePath path)
{
var result = self.GetNodeAndResource(path);
return ((Node)result[0], (Resource)result[1], (NodePath)result[2]);
}
public static void QueueFreeChildren(this Node node) => node.GetChildren().ToList().ForEach(item=>item.QueueFree());
public static void QueueFreeChildren(this Node node, Func<Node,bool> predicate) => node.GetChildren().Where(predicate).ToList().ForEach(item=>item.QueueFree());
}

View File

@@ -0,0 +1 @@
uid://bklfdjfp02pav

View File

@@ -0,0 +1,177 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Godot;
using Godot.Collections;
using SJK.Functional;
namespace SJK.GodotHelpers.Raycasts;
public record CollisionResultBase(
GodotObject Collider,
int ColliderId,
Rid Rid,
int Shape
);
public record RaycastResult3D(
GodotObject Collider,
int ColliderId,
Rid Rid,
int Shape,
Vector3 Position,
Vector3 Normal
) : CollisionResultBase(Collider, ColliderId, Rid, Shape);
public record RaycastResult2D(
GodotObject Collider,
int ColliderId,
Rid Rid,
int Shape,
Vector2 Position,
Vector2 Normal
) : CollisionResultBase(Collider, ColliderId, Rid, Shape);
public record ShapeCastResult3D(
GodotObject Collider,
int ColliderId,
Rid Rid,
int Shape,
Vector3 Point,
Vector3 Normal,
int CollisionCount
) : CollisionResultBase(Collider, ColliderId, Rid, Shape);
public record ShapeCastResult2D(
GodotObject Collider,
int ColliderId,
Rid Rid,
int Shape,
Vector2 Point,
Vector2 Normal,
int CollisionCount
) : CollisionResultBase(Collider, ColliderId, Rid, Shape);
public record PointQueryResult3D(
GodotObject Collider,
int ColliderId,
Rid Rid,
int Shape
) : CollisionResultBase(Collider, ColliderId, Rid, Shape);
public record PointQueryResult2D(
GodotObject Collider,
int ColliderId,
Rid Rid,
int Shape
) : CollisionResultBase(Collider, ColliderId, Rid, Shape);
#nullable enable
public static class RaycastExtensions
{
// --- 3D ---
public static RaycastResult3D? RaycastEx(this PhysicsDirectSpaceState3D space, Vector3 from, Vector3 to, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false)
{
var query = new PhysicsRayQueryParameters3D
{
From = from,
To = to,
CollisionMask = collisionMask,
HitFromInside = hitFromInside,
};
if (exclude != null)
query.Exclude = new Array<Rid>(exclude);
var result = space.IntersectRay(query);
if (result.Count == 0)
return null;
return new RaycastResult3D(
Collider: result["collider"].AsGodotObject(),
ColliderId: result["collider_id"].AsInt32(),
Rid: (Rid)result["rid"],
Shape: result["shape"].AsInt32(),
Position: result["position"].AsVector3(),
Normal: result["normal"].AsVector3()
);
}
public static bool RaycastHitEx(this PhysicsDirectSpaceState3D space, Vector3 from, Vector3 to,[NotNullWhen(true)] out RaycastResult3D hit, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false)
{
hit = space.RaycastEx(from, to, collisionMask, exclude, hitFromInside)!;
return hit is not null;
}
// --- 2D ---
public static RaycastResult2D? RaycastEx(this PhysicsDirectSpaceState2D space, Vector2 from, Vector2 to, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false)
{
var query = new PhysicsRayQueryParameters2D
{
From = from,
To = to,
CollisionMask = collisionMask,
HitFromInside = hitFromInside,
};
if (exclude != null)
query.Exclude = new Array<Rid>(exclude);
var result = space.IntersectRay(query);
if (result.Count == 0)
return null;
return new RaycastResult2D(
Collider: result["collider"].AsGodotObject(),
ColliderId: result["collider_id"].AsInt32(),
Rid: (Rid)result["rid"],
Shape: result["shape"].AsInt32(),
Position: result["position"].AsVector2(),
Normal: result["normal"].AsVector2()
);
}
public static IOption<RaycastResult2D> RaycastOptionEx(this PhysicsDirectSpaceState2D space, Vector2 from, Vector2 to, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false)
=> RaycastEx(space, from, to, collisionMask, exclude, hitFromInside).ToOption();
public static bool RaycastHitEx(this PhysicsDirectSpaceState2D space, Vector2 from, Vector2 to, out RaycastResult2D hit, uint collisionMask = uint.MaxValue, Rid[]? exclude = null, bool hitFromInside = false)
{
hit = space.RaycastEx(from, to, collisionMask, exclude, hitFromInside)!;
return hit is not null;
}
// 3D Shape Cast
public static List<CollisionResultBase> IntersectShapeEx(this PhysicsDirectSpaceState3D space, PhysicsShapeQueryParameters3D query, int maxResults = 32)
{
var results = space.IntersectShape(query, maxResults);
var list = new List<CollisionResultBase>(results.Count);
foreach (var dict in results)
{
list.Add(new CollisionResultBase(
Collider: dict["collider"].AsGodotObject(),
ColliderId: dict["collider_id"].AsInt32(),
Rid: (Rid)dict["rid"],
Shape: dict["shape"].AsInt32()
));
}
return list;
}
// 2D Shape Cast
public static List<CollisionResultBase> IntersectShapeEx(this PhysicsDirectSpaceState2D space, PhysicsShapeQueryParameters2D query, int maxResults = 32)
{
var results = space.IntersectShape(query, maxResults);
var list = new List<CollisionResultBase>(results.Count);
foreach (var dict in results)
{
list.Add(new CollisionResultBase(
Collider: dict["collider"].AsGodotObject(),
ColliderId: dict["collider_id"].AsInt32(),
Rid: (Rid)dict["rid"],
Shape: dict["shape"].AsInt32()
));
}
return list;
}
}

View File

@@ -0,0 +1 @@
uid://b8b5eyg6l31o1

View File

@@ -0,0 +1,121 @@
using Godot;
using Godot.Collections;
using System;
using System.Collections.Generic;
using System.Linq;
using Array = Godot.Collections.Array;
namespace SJK.GodotHelpers;
/// <summary>
/// Strongtyped view of Godot's get_method_list / get_property_list output.
/// </summary>
public static class Reflector
{
/* ──────────── Typed records ──────────── */
public readonly record struct ArgInfo(
string Name,
Variant.Type Type,
PropertyHint Hint,
string HintString,
Variant DefaultValue);
public readonly record struct MethodInfoEx(
string Name,
IReadOnlyList<ArgInfo> Args,
IReadOnlyList<Variant> DefaultArgs,
MethodFlags Flags,
int Id,
ArgInfo? ReturnValue);
public readonly record struct PropertyInfoEx(
string Name,
string ClassName,
Variant.Type Type,
PropertyHint Hint,
string HintString,
PropertyUsageFlags Usage);
/* ──────────── Public helpers ──────────── */
public static List<MethodInfoEx> GetMethodsListEx(this GodotObject godotObject)=>GetMethods(godotObject);
public static List<MethodInfoEx> GetMethods(GodotObject obj)
{
var raw = obj.GetMethodList();
var list = new List<MethodInfoEx>(raw.Count);
foreach (Dictionary dict in raw)
{
// — Parse args —
var argsRaw = (Array)dict["args"];
var args = new List<ArgInfo>(argsRaw.Count);
foreach (Dictionary a in argsRaw)
args.Add(ParseArg(a));
// — Parse return (may be empty) —
ArgInfo? ret = null;
if (dict.TryGetValue("return", out var retRaw) && retRaw.AsGodotDictionary() is Dictionary rd && rd.Count > 0)
ret = ParseArg(rd);
list.Add(new MethodInfoEx(
Name: (string)dict["name"],
Args: args,
DefaultArgs: (Array)dict["default_args"],
Flags: (MethodFlags)(int)dict["flags"],
Id: (int)dict["id"],
ReturnValue: ret
));
}
return list;
}
public static bool TryGetMethodInfo(this GodotObject godotObject, string property, out MethodInfoEx info){
foreach (var item in GetMethods(godotObject))
{
if (item.Name == property){
info = item;
return true;
}
}
info = default;
return false;
}
public static List<PropertyInfoEx> GetPropertyListEx(this GodotObject godotObject)=>GetProperties(godotObject);
public static List<PropertyInfoEx> GetProperties(GodotObject obj)
{
var raw = obj.GetPropertyList();
var list = new List<PropertyInfoEx>(raw.Count);
foreach (Dictionary dict in raw)
{
list.Add(new PropertyInfoEx(
Name: (string)dict["name"],
ClassName: (string)dict["class_name"],
Type: (Variant.Type)(int)dict["type"],
Hint: (PropertyHint)(int)dict["hint"],
HintString: (string)dict["hint_string"],
Usage: (PropertyUsageFlags)(int)dict["usage"]
));
}
return list;
}
public static PropertyInfoEx GetProperty(this IEnumerable<PropertyInfoEx> properties, string name)=>properties.FirstOrDefault(item=>item.Name == name);
public static bool TryGetPropertyInfo(this GodotObject godotObject, string property, out PropertyInfoEx info){
foreach (var item in GetProperties(godotObject))
{
if (item.Name == property){
info = item;
return true;
}
}
info = default;
return false;
}
/* ──────────── Internals ──────────── */
private static ArgInfo ParseArg(Dictionary d) => new(
Name: (string)d["name"],
Type: (Variant.Type)(int)d["type"],
Hint: (PropertyHint)(int)d["hint"],
HintString: (string)d["hint_string"],
DefaultValue: d.TryGetValue("default_value", out var dv) ? dv : default);
}

View File

@@ -0,0 +1 @@
uid://bis0ef0hnuxin

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using Godot;
using Godot.Collections;
namespace SJK.GodotHelpers;
public static class VariantUtils
{
public static Variant SafeToVariant(object value)
{
return value switch
{
null => new Variant(),
bool b => Variant.From(b),
int i => Variant.From(i),
long l => Variant.From((int)l), // Godot Variant only supports 32-bit int
float f => Variant.From(f),
double d => Variant.From((float)d),
string s => Variant.From(s),
Vector2 v2 => Variant.From(v2),
Vector2I v2i => Variant.From(v2i),
Vector3 v3 => Variant.From(v3),
Vector3I v3i => Variant.From(v3i),
Vector4 v4 => Variant.From(v4),
Vector4I v4i => Variant.From(v4i),
Rect2 rect2 => Variant.From(rect2),
Rect2I rect2i => Variant.From(rect2i),
Quaternion q => Variant.From(q),
Basis basis => Variant.From(basis),
Transform2D t2d => Variant.From(t2d),
Transform3D t3d => Variant.From(t3d),
Color color => Variant.From(color),
Plane plane => Variant.From(plane),
Aabb aabb => Variant.From(aabb),
GodotObject go => Variant.From(go),
byte[] bytes => Variant.From(bytes),
StringName sn => Variant.From(sn),
NodePath np => Variant.From(np),
Callable call => Variant.From(call),
Signal sig => Variant.From(sig),
Dictionary dict => Variant.From(dict),
Godot.Collections.Array array => Variant.From(array),
_ => throw new InvalidCastException($"Unsupported type '{value?.GetType().FullName}' for Variant conversion.")
};
}
public static Type GetSystemType(this Variant.Type type) => type switch
{
Variant.Type.Nil => typeof(object),
Variant.Type.Bool => typeof(bool),
Variant.Type.Int => typeof(int),
Variant.Type.Float => typeof(float),
Variant.Type.String => typeof(string),
Variant.Type.Vector2 => typeof(Vector2),
Variant.Type.Vector2I => typeof(Vector2I),
Variant.Type.Rect2 => typeof(Rect2),
Variant.Type.Rect2I => typeof(Rect2I),
Variant.Type.Vector3 => typeof(Vector3),
Variant.Type.Vector3I => typeof(Vector3I),
Variant.Type.Vector4 => typeof(Vector4),
Variant.Type.Vector4I => typeof(Vector4I),
Variant.Type.Transform2D => typeof(Transform2D),
Variant.Type.Transform3D => typeof(Transform3D),
Variant.Type.Basis => typeof(Basis),
Variant.Type.Quaternion => typeof(Quaternion),
Variant.Type.Aabb => typeof(Aabb),
Variant.Type.Color => typeof(Color),
Variant.Type.Plane => typeof(Plane),
Variant.Type.StringName => typeof(StringName),
Variant.Type.NodePath => typeof(NodePath),
Variant.Type.Rid => typeof(Rid),
Variant.Type.Object => typeof(GodotObject),
Variant.Type.Callable => typeof(Callable),
Variant.Type.Signal => typeof(Signal),
Variant.Type.Dictionary => typeof(Godot.Collections.Dictionary),
Variant.Type.Array => typeof(Godot.Collections.Array),
_ => typeof(object)
};
}

View File

@@ -0,0 +1 @@
uid://b18a1dp6f8tsv

132
src/VoxelGrid/BeltPort.cs Normal file
View File

@@ -0,0 +1,132 @@
namespace ChickenGameTest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using SJK.Functional;
[Tool]
[Meta(typeof(IAutoNode))]
public partial class BeltPort : Node3D, IBeltPort {
public override void _Notification(int what) => this.Notify(what);
[Dependency] public IVoxelGridRegistry Grid => this.DependOn<IVoxelGridRegistry>();
[Export] public Direction Face { get; set; } = default!;
[Export] public int Width { get; set; } = default!;
[Export] public PortAccess Access { get; set; } = default!;
[Export] public Path3D Path { get; set; } = default!;
[Dependency] public IItemRenderer ItemRenderer => this.DependOn<IItemRenderer>();
public BeltPortProfile Profile => new(GridTransform3D.FromGodot(GlobalTransform), Width, Access);
[Dependency] public IItemTransferAnimator ItemTransferAnimator => this.DependOn<IItemTransferAnimator>(()=> new CurveItemTransfer(){Curve3D = Path.Curve,Tree = GetTree(), ItemRenderer = ItemRenderer, Transform3D = GlobalTransform});
public void OnResolved()
{
if (Engine.IsEditorHint())
{
return;
}
Grid.Register<IBeltPort>(this, [.. (this as IBeltPort).Points()]);
}
public override void _Process(double delta)
{
DebugDraw3D.DrawLine(GlobalPosition,GlobalTransform * Face.ToVector(), Colors.Red);
if (Engine.IsEditorHint())
{
return;
}
for (int i = 0; i < _itemsDummys.Count; i++)
{
_itemsDummys[i] = (_itemsDummys[i].i+(float)delta,_itemsDummys[i].beltItem);
GD.Print(_itemsDummys[i].i);
ItemRenderer.UpdateTransform(_itemsDummys[i].beltItem,GlobalTransform * Path.Curve.SampleBakedWithRotation(_itemsDummys[i].i));
if (_itemsDummys[i].i > Path.Curve.GetBakedLength())
{
_itemsDummys[i].beltItem.Dispose();
_itemsDummys.RemoveAt(i);
i--;
}
}
}
public bool CanAccept(IBeltItem item, LaneSpan laneSpan, float beltT)
{
return true;
throw new System.NotImplementedException();
}
List<(float i,IBeltItem beltItem)> _itemsDummys = [];
public bool TryInsert(IBeltItem item, LaneSpan laneSpan, float beltT)
{
var tween = ItemTransferAnimator.StartTransfer(new(item,beltT){LaneSpan = laneSpan}, () => GD.Print("Done"));
// GD.Print("gg "+laneSpan);
tween.TweenCallback(Callable.From(item.Dispose));
// GD.Print(laneSpan);
// GD.Print(item.ToString());
// _itemsDummys.Add((0,item));
// item.Dispose();
return true;
throw new System.NotImplementedException();
}
}
public interface IItemTransferAnimator
{
Tween StartTransfer(
ConveyorSlice item,
Action onFinished = null);
}
public class InstanceItemTransfer : IItemTransferAnimator
{
public SceneTree Tree = default!;
public Func<ConveyorSlice, bool> AcceptFunc = default!;
public Tween StartTransfer(ConveyorSlice item, Action onFinished = null)
{
AcceptFunc(item);
onFinished?.Invoke();
return Tree.CreateTween();
throw new NotImplementedException();
}
}
public class CurveItemTransfer : IItemTransferAnimator
{
public Curve3D Curve3D = default!;
public IItemRenderer ItemRenderer = default!;
public Func<ConveyorSlice, bool> AcceptFunc = default!;
public SceneTree Tree = default!;
public Transform3D Transform3D;
public Tween AnimateAlongCurve(
ConveyorSlice item,
Curve3D curve,
float duration,
IItemRenderer renderer)
{
float length = curve.GetBakedLength();
var tween = Tree.CreateTween();
tween.TweenMethod(
Callable.From<float>(t =>
{
//Transform is not being set when using update transform in a tween, but can set position
// renderer.UpdateTransform(item, Transform3D * curve.SampleBakedWithRotation(t),1/duration);
(renderer as TestItemRendered)._items[item.Item].Transform =Transform3D *Curve3D.SampleBakedWithRotation(t).Translated(-Curve3D.SampleBakedWithRotation(t).Basis.X * item.LaneSpan.Start);
// GD.PrintS(Transform3D * curve.SampleBakedWithRotation(t).Origin,1/duration,(renderer as TestItemRendered)._items[item].GlobalPosition);
}),
0f,
length,
duration
);
return tween;
}
public Tween StartTransfer(ConveyorSlice item, Action onFinished = null){
var tween = AnimateAlongCurve(item,Curve3D,1,ItemRenderer);
if (onFinished is not null){
tween.TweenCallback(Callable.From(onFinished));
}
return tween;
}
}

View File

@@ -0,0 +1 @@
uid://ee5aoxi8mjnw

View File

@@ -0,0 +1,114 @@
namespace ChickenGameTest;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
[Meta(typeof(IAutoNode))]
public partial class ConveyorItemRender : Node
{
public override void _Notification(int what) => this.Notify(what);
[Export] protected Path3D Path3D { get; set; } = default!;
[Export] protected TestItemConveyor ItemConveyor { get; set; } = default!;
[Chickensoft.AutoInject.Dependency] protected IItemRenderer Items => this.DependOn<IItemRenderer>();
private Chickensoft.Sync.Primitives.AutoList<ConveyorSlice>.Binding binding = default!;
public override async void _Ready()
{
base._Ready();
if (!ItemConveyor.IsNodeReady())
{
await ToSignal(ItemConveyor, Node.SignalName.Ready);
}
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) =>
{
// itemsRenders[i.Item] = new MeshInstance3D(){Mesh = new BoxMesh()};
// AddSibling(itemsRenders[i.Item]);
// itemsRenders[i.Item].Transform = Path3D.Curve.SampleBakedWithRotation(i.BeltT);
// GD.Print($"Item : {i},{v} added");
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)));
return;
if (itemsRenders.TryGetValue(a.Item, out var node))
{
if (tweens.TryGetValue(a.Item, out var t))
{
t.Kill();
}
tweens[a.Item] = t = CreateTween();
t.TweenProperty(node, "transform",Path3D.Curve.SampleBakedWithRotation(b.BeltT), .25f);
// node.Transform = Path3D.Curve.SampleBakedWithRotation(b.BeltT);
}
});
}
private Dictionary<IBeltItem, Node3D> itemsRenders = [];//This would be an item server for reuse via depency
private Dictionary<IBeltItem, Tween> tweens = [];//This would be an item server for reuse via depency
protected override void Dispose(bool disposing)
{
binding.Dispose();
base.Dispose(disposing);
}
}
public interface IItemRenderer
{
// Node3D GetVisualNode(IBeltItem beltItem);
void UpdateTransform(IBeltItem beltItem, Transform3D newTransform);
void UpdateTransform(IBeltItem beltItem, Transform3D newTransform, float time);
void Remove(IBeltItem beltItem);
}
public partial class TestItemRendered : Node3D, IItemRenderer
{
public Dictionary<IBeltItem, Node3D> _items = [];
private ConditionalWeakTable<IBeltItem, Tween> _tweens = [];
public void Remove(IBeltItem beltItem)
{
if (_items.TryGetValue(beltItem, out var node)){
node.QueueFree();
}
_items.Remove(beltItem);
}
public void UpdateTransform(IBeltItem beltItem, Transform3D newTransform, float time ){
if (!_items.TryGetValue(beltItem, out var node))
{
_items.Add(beltItem,node = beltItem.CreateItemVisual());
AddChild(node);
beltItem.Disposed += _ =>{node.QueueFree();_items.Remove(beltItem);};
node.Transform = newTransform;
return;
}
if (_tweens.TryGetValue(beltItem, out var tween))
{
tween.Kill();
_tweens.Remove(beltItem);
}
// GD.Print(newTransform);
tween = GetTree().CreateTween().BindNode(node);
tween.TweenProperty(node, "transform", newTransform, time);
_tweens.Add(beltItem,tween);
}
public void UpdateTransform(IBeltItem beltItem, Transform3D newTransform) => UpdateTransform(beltItem,newTransform,.25f);
}

View File

@@ -0,0 +1 @@
uid://bjuntmf2sjynp

View File

@@ -0,0 +1,83 @@
namespace ChickenGameTest;
using System;
using Chickensoft.AutoInject;
using Chickensoft.GodotNodeInterfaces;
using Chickensoft.Introspection;
using Godot;
public interface IEquipment : IProvide<IEquipmentContext>//INode3D,
{
EquipmentId Id { get; set; }
}
[Tool]
[Meta(typeof(IAutoNode))]
public partial class Equipment() : Node3D, IEquipment
{
public override void _Notification(int what)
{
if (what == NotificationTransformChanged)
{
OnNotificationTransformChanged();
}
// if (Engine.IsEditorHint())
// {
// return;
// }
this.Notify(what);
}
public EquipmentId Id { get; set; } = new(Guid.NewGuid());
[Signal] public delegate void TickEventHandler();
[Export]public Vector3I GridPos { get; set; }
[Dependency] protected IVoxelGridQuery<LayeredEquipment> Grid => this.DependOn<IVoxelGridQuery<LayeredEquipment>>();
protected IEquipmentContext _equipmentContext { get; set; } = default!;
public override void _Ready()
{
this.SetNotifyTransform(true);
base._Ready();
// if (Engine.IsEditorHint())
// {
// return;
// }
_equipmentContext = new eqitpTest(this);
this.Provide();
Timer timer = new Timer(){WaitTime = .25f, Autostart = true};
AddChild(timer);
timer.Timeout += EmitSignalTick;
// timer.Timeout += () => Position += Vector3.One;
}
public void OnResolved()
{
GD.Print(Grid);
(Grid as EquipmentVoxelGrid).AddEquipment(GridPos, this);
GD.Print("Added Self");
}
IEquipmentContext IProvide<IEquipmentContext>.Value() => _equipmentContext;
public void OnNotificationTransformChanged()
{
GD.Print("Transform changed, now" , Position);
var newPos = new Vector3I(Mathf.RoundToInt(Position.X),Mathf.RoundToInt(Position.Y),Mathf.RoundToInt(Position.Z));
if (newPos == GridPos)
{
return;
}
GridPos = newPos;
// Transform = Transform.Origin = newPos;;
}
}
public record EquipmentId(Guid Id);
public interface IEquipmentContext
{
Equipment GetEquipment();
EquipmentId GetEquipmentId();
}
public record eqitpTest(Equipment Equipment) : IEquipmentContext
{
public Equipment GetEquipment() => Equipment;
public EquipmentId GetEquipmentId() => Equipment.Id;
}

View File

@@ -0,0 +1 @@
uid://bhh1c4a5gep6o

View File

@@ -0,0 +1,788 @@
namespace ChickenGameTest;
using Chickensoft.Introspection;
using Chickensoft.AutoInject;
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using Chickensoft.Sync.Primitives;
using SJK.Functional;
using System.Diagnostics.CodeAnalysis;
public interface IMovementConveyor
{
IBeltPort StartPort { get; }
// IBeltSlotProfile StartPort { get; }
IBeltPort EndPort { get; }
// IBeltSlotProfile EndPort { get; }
// IOption<IBeltSlotProfile> InputPort { get; }
// IOption<IBeltSlotProfile> OutputPort { get; }
// IEnumerable<IBeltSlotProfile> GetPorts();
IEnumerable<IBeltPort> GetPorts();
IAutoValue<float> SpeedValue { get; }
float SpeedMagnitude { get; set; }
float SignedSpeed { get; set; }
bool IsReversed { get; set; }
float Length {get;set;}
// float GetAvailableTravel(ItemConveyor.BeltDirection beltDirection, LaneSpanT itemSpan, float maxDistance);
// IBeltSlotProfile GetPortFacingStart();
// IBeltSlotProfile GetPortFacingEnd();
ItemConveyor.BeltDirection GetBeltDirection();
// Option<ConveyorSlice> GetItemTowardStart();
// Option<ConveyorSlice> GetItemTowardEnd();
// Option<ConveyorSlice> GetItemTowardInput();
// Option<ConveyorSlice> GetItemTowardOutput();
IEnumerable<Sorted1DList<ConveyorSlice>.ItemHandle> EnumerateTowardEnd();
IEnumerable<Sorted1DList<ConveyorSlice>.ItemHandle> EnumerateTowardStart();
ItemConveyor.IBeltMovement GetMovementPolicy();
// IList<ConveyorSlice> Items { get; }
IOption<IBeltPort> GetPortFacing(IBeltPort slot);
BeltObstacle GetDistanceToNextItem(ItemConveyor.BeltDirection beltDirection, float itemBeltT, float maxDistToCheck, LaneSpan laneSpan, HashSet<IMovementConveyor>? visted = null);
ConveyorPort CreatePort(BeltPortProfile profile, ItemConveyor.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);
}
[Meta(typeof(IAutoNode))]
public partial class ItemConveyor : Node, IVoxelNode
{
// public record ItemPair(IBeltItem Item, float BeltT);
public override void _Notification(int what) => this.Notify(what);
[Export] public int Length { get; set; } = 1;
// [Export] public float Speed { get; set; } = .05f;
private readonly AutoValue<float> _speed = new(.05f);
public IAutoValue<float> Speed => _speed;
public float SignedSpeed
{
get => _speed.Value;
set => _speed.Value = value;
}
public float SpeedMagnitude
{
get => Mathf.Abs(_speed.Value);
set => _speed.Value = Mathf.Abs(value) * Mathf.Sign(_speed.Value);
}
public bool IsReversed { get => Mathf.Sign(_speed.Value) < 0; set => _speed.Value = SpeedMagnitude * (value ? -1 : 1); }
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
[Dependency] public IBeltMovement MovementSystem => this.DependOn<IBeltMovement>(() => new IndividualMovement());
public const float ITEMSIZE = .2f;
private const float ITEMHALFSIZE = ITEMSIZE / 2f;
//Basci port for simple conveyor, asuming no rever,
private List<ConveyorPort> _ports = [
new(){//EjectFace
Face = Direction.Front,
Direction= PortAccess.InOut,
LocalOffset = Vector3I.Zero,
BeltT = new BeltTEnd(ConveyorEnd.End),
PullPush = TransferMode.PushPull,
},
new(){//PullFace
Face = Direction.Back,
Direction= PortAccess.InOut,
LocalOffset = Vector3I.Zero,
BeltT = new BeltTEnd(ConveyorEnd.Start),
PullPush = TransferMode.PushPull//Or none, PlateUp has grtabber and none grabby varietns
},
new(){//PullFace
Face = Direction.Right,
Direction= PortAccess.BiDirectional,
LocalOffset = Vector3I.Zero,
BeltT = new BeltTOffset(.5f),
},
new(){//PullFace
Face = Direction.Left,
Direction= PortAccess.BiDirectional,
LocalOffset = Vector3I.Zero,
BeltT = new BeltTOffset(.5f),
},
new(){//PullFace
Face = Direction.Up,
Direction= PortAccess.BiDirectional,
LocalOffset = Vector3I.Zero,
BeltT = new BeltTOffset(.5f),
}
];
public ConveyorPort GetInputPort() => _ports.First(item => (item.PullPush & TransferMode.Pull)>0 && item.BeltT == (SignedSpeed >= 0 ? new BeltTEnd(ConveyorEnd.Start):new BeltTEnd(ConveyorEnd.End)));//Cache this
public ConveyorPort GetOutputPort() => _ports.First(item => (item.PullPush & TransferMode.Push)>0 && item.BeltT == (SignedSpeed < 0 ? new BeltTEnd(ConveyorEnd.Start):new BeltTEnd(ConveyorEnd.End)));//Cache this
public ConveyorPort GetStartPort() => _ports.First(item => item.BeltT == new BeltTEnd(ConveyorEnd.Start));
public ConveyorPort GetEndPort() => _ports.First(item => item.BeltT == new BeltTEnd(ConveyorEnd.End));
// public ConveyorPort GetPortInDirectionOfTravel() => Speed >= 0 ? GetOutputPort() : GetInputPort();
//needs better name
// private IEnumerable<ConveyorPort> ActivePorts(TransferMode p) => _ports.Where(item => item.PullPush == p);
public void OnResolved()
{
// GD.Print(SlotExtesion.MapSlotToFacingSlot(Vector3I.Zero,Direction.Back,2,new(2,0,1),Direction.Front,3));
// GD.Print(SlotExtesion.MapSlotToFacingSlot(new(2,0,1),Direction.Front,3,Vector3I.Zero,Direction.Back,2));
GridRegistry.Register(this);
Timer timer = new Timer() { WaitTime = .25f, Autostart = true };//TEST
AddChild(timer);//TEST
timer.Timeout += OnTick;//TEST
for (int i = 0; i < _ports.Count; i++)
{
var port = _ports[i];
if (port.Access.HasFlag(PortAccess.In)){
port.AcceptItemFunc = (item, belt) =>
{
GD.PrintS("============================",port.Access,item,belt,port.BeltT);
if (port.BeltT is BeltTEnd end)
{
return TryInsertAtEnd(end.End, item);
}
if (port.BeltT is BeltTOffset offset && TryFindLocalInsertion(offset.T,ITEMHALFSIZE,out var point))
{
InsertInMiddleWithoutCheck(item, point);
return true;
}
return false;
};
_ports[i] = port;
}
}
// MovementSystem.OnItemMoved += static (item, segments, delta) => GD.PrintS(item, string.Join(',', segments.ToArray().Select(i => $"({i.Conveyor},{i.StartT},{i.EndT}")));
}
public override void _ExitTree()
{
base._ExitTree();
GridRegistry.UnRegister(Id);
}
public void OnTick()//TEMP METHOOD FOR TESTING
{
if (Input.IsActionPressed("ui_up"))
{
SignedSpeed = -SignedSpeed;
}
if (VoxelPosition == Vector3I.Forward*-2 && TryInsertAtEnd(ConveyorEnd.Start, new TestItem(){Height = 1, Width = 1, Temp = 1}))
InsertAtEndWithoutCheck(ConveyorEnd.Start,new TestItem(){Height = 1, Width = 1, Temp = 1});
MovementSystem.AdvanceBelt(this,1f);
return;
// if (_items.Count <= 0)
// {
// return;
// }
// if (Speed == 0)
// {
// return;
// }
// GD.Print("Thing: " + Speed + " "+ GetConveyorPortFacing(GetOutputPort()));
// var distToEnd = DistanceFromEnd(Speed < 0 ? ConveyorEnd.Start : ConveyorEnd.End);
// var maxSpeed = Mathf.Min(Mathf.Abs(Speed), distToEnd) * Mathf.Sign(Speed);
// if (maxSpeed == 0)
// {
// return;
// }
// // GD.Print()
// for (int i = 0; i < _items.Count; i++)
// {
// _items[i] = _items[i] with { BeltT = Mathf.Clamp(_items[i].BeltT + maxSpeed, 0, Length) };
// GD.Print($"Item:{_items[i].Item}, Position:{_items[i].BeltT}");
// }
// // _items.ForEach((item) => item = item = Mathf.Clamp(item.Position + maxSpeed, 0, Length));
// var list = _items.Where(item=> Speed>=0?item.BeltT >= Length:item.BeltT <= 0).ToList();
// // GD.Print(list.Count);
// foreach (var item in list)
// {
// GD.Print($"Item:{item.Item}, Position:{item.BeltT}");
// //TEST
// var v = GetConveyorPortFacing(GetOutputPort());
// if (v.HasValue)
// {
// GD.PrintS(v.Value.Port.LocalOffset,v.Value.Port.BeltT,v.Value.Port.Direction,v.Value.Port.PullPush,v.Value.Port.Face);
// if(v.Value.Conveyor.TryInsertAtEnd((v.Value.Port.BeltT as BeltTEnd).End, item.Item))
// {
// GD.Print("Item Moved");
// _items.Remove(item);
// }
// }
// //Move Item Into Other COnveyor with offset, this means a item could therolitcly move two the end of another conveyor, but need to decide if that should keep moving recurively, opr make items only move one conveyor max at atime
// }
}
public float? GetAvailableTravelForFrontItem(bool accountForNextConveyor = false) => GetBeltDirection() switch
{
BeltDirection.TowardStart => AnyItems() ? GetAvailableTravel(0, BeltDirection.TowardStart, accountForNextConveyor) : null,
BeltDirection.TowardEnd => AnyItems() ? GetAvailableTravel(ItemsCount - 1, BeltDirection.TowardEnd, accountForNextConveyor) : null,
BeltDirection.NotMoving => null,
_ => throw new NotSupportedException(),
};
public float GetAvailableSpaceFromEnd(ConveyorEnd end) => end switch
{
ConveyorEnd.Start => AnyItems()?_items[0].BeltT - ITEMSIZE:Length -ITEMHALFSIZE,
ConveyorEnd.End => AnyItems()?_items[^1].BeltT + ITEMSIZE : ITEMHALFSIZE,
_ => throw new NotSupportedException(),
};
public float GetAvailableTravel(
int itemIndex,
BeltDirection direction,
bool accountForNextConveyor = false
)
{
if (itemIndex < 0 || itemIndex >= _items.Count)
throw new IndexOutOfRangeException(
$"{nameof(itemIndex)}={itemIndex}, Count={_items.Count}"
);
var item = _items[itemIndex];
// Determine neighbor and boundary
bool towardEnd = direction == BeltDirection.TowardEnd;
float limit;
if (towardEnd)
{
var other = GetConveyorPortFacing(GetEndPort());
// Next item or belt end
limit = (itemIndex + 1 < _items.Count)
? _items[itemIndex + 1].BeltT - ITEMSIZE
: Length + (accountForNextConveyor && other.HasValue && other.Value.Port.BeltT is BeltTEnd end ? other.Value.Conveyor.GetAvailableSpaceFromEnd(end.End) : -ITEMHALFSIZE);
}
else
{
var other = GetConveyorPortFacing(GetStartPort());
// Previous item or belt start
limit = (itemIndex - 1 >= 0)
? _items[itemIndex - 1].BeltT + ITEMSIZE
: accountForNextConveyor && other.HasValue && other.Value.Port.BeltT is BeltTEnd end ? other.Value.Conveyor.GetAvailableSpaceFromEnd(end.End) : ITEMHALFSIZE;
}
float available = towardEnd
? limit - item.BeltT
: item.BeltT - limit;
return Mathf.Max(0f, available);
}
public enum ConveyorEnd { Start, End }
public static ConveyorEnd SwapEnd(ConveyorEnd end) => end switch
{
ConveyorEnd.Start => ConveyorEnd.End,
ConveyorEnd.End => ConveyorEnd.Start,
_ => throw new NotSupportedException($"{end}"),
};
public float DistanceFromEnd(ConveyorEnd end) => end switch
{
ConveyorEnd.Start => _items.Count == 0 ? Length : _items[0].BeltT,
ConveyorEnd.End => _items.Count == 0 ? Length : Length - _items[^1].BeltT,
_ => throw new NotSupportedException($"{end}"),
};
public float GetTravelDistanceInDirection(BeltDirection direction) => direction switch
{
BeltDirection.TowardEnd => _items.Count == 0 ? Length : _items[0].BeltT,
BeltDirection.TowardStart => _items.Count == 0 ? Length : Length - _items[^1].BeltT,
BeltDirection.NotMoving => 0,
_ => throw new NotSupportedException($"{direction}"),
};
public enum BeltDirection : sbyte {
TowardStart = -1,
NotMoving = 0,
TowardEnd = 1
}
public bool TryFindLocalInsertion(
float beltT,
float maxDistance,
out float resultT
)
{
throw new NotImplementedException();
// float minAllowed = Mathf.Max(ITEMHALFSIZE, beltT - maxDistance);
// float maxAllowed = Mathf.Min(Length - ITEMHALFSIZE, beltT + maxDistance);
// if (minAllowed > maxAllowed)
// {
// resultT = default;
// return false;
// }
// if (_items.Count == 0)
// {
// resultT = Mathf.Clamp(beltT, minAllowed, maxAllowed);
// return true;
// }
// int index = _items.BinarySearch(
// new(null!, beltT),
// Comparer<ConveyorSlice>.Create(
// (a, b) => a.BeltT.CompareTo(b.BeltT)
// )
// );
// if (index < 0)
// {
// index = ~index;
// }
// float gapMin = index > 0
// ? _items[index - 1].BeltT + ITEMSIZE
// : ITEMHALFSIZE;
// float gapMax = index < _items.Count
// ? _items[index].BeltT - ITEMSIZE
// : Length - ITEMHALFSIZE;
// // Intersect gap with allowed window
// gapMin = Mathf.Max(gapMin, minAllowed);
// gapMax = Mathf.Min(gapMax, maxAllowed);
// if (gapMin > gapMax)
// {
// resultT = default;
// return false;
// }
// resultT = Mathf.Clamp(beltT, gapMin, gapMax);
// return true;
}
public BeltDirection GetBeltDirection() => _speed.Value switch
{
0 => BeltDirection.NotMoving,
> 0 => BeltDirection.TowardEnd,
< 0 => BeltDirection.TowardStart,
_ => throw new NotSupportedException($"{nameof(Speed)} with value {Speed} is not Supported")
};
/// <summary>
/// Returns the Item that would be Leading the Belt based off the direction of the belt, or null if no items or Speed is zero.
/// </summary>
/// <returns>The Leading Item</returns> <summary>
///
/// </summary>
/// <returns></returns>
public ConveyorSlice? GetFirstItem() => GetBeltDirection() switch
{
BeltDirection.TowardStart => _items.Any() ? _items[0] : null,
BeltDirection.TowardEnd => _items.Any() ? _items[^1] : null,
BeltDirection.NotMoving => null,
_ => null
};
/// <summary>
/// Returns the Last item that would be oppsite the Leading Item, or null if there is no items or Speed is zero.
/// </summary>
/// <returns>The last Item in Secuance</returns> <summary>
///
/// </summary>
/// <returns></returns>
public ConveyorSlice? GetLastItem() => GetBeltDirection() switch
{
BeltDirection.TowardStart => _items.Any() ? _items[0] : null,
BeltDirection.TowardEnd => _items.Any() ? _items[^1] : null,
BeltDirection.NotMoving => null,
_ => null
};
public bool AnyItems() => _items.Any();
public int ItemsCount => _items.Count;
private void InsertInMiddleWithoutCheck(IBeltItem item, float beltT)
{
throw new NotImplementedException();
_items.Add(new(item, beltT));
// _items.Sort(Comparer<ConveyorSlice>.Create(
// (a, b) => a.BeltT.CompareTo(b.BeltT)));
}
// public float SpaceToInsertFromEnd(ConveyorEnd end)//Needs to know if belt space exists in next/previous
// {
// var distToEnd = DistanceFromEnd(end);
// var other = GetConveyorPortFacing(GetOutputPort());
// if (!other.HasValue || other.Value.Port.BeltT != new BeltTEnd(SwapEnd(end)) || other.Value.Port.Direction == PortAccess.Out)
// {
// return Mathf.Max(0, distToEnd - ITEMHALFSIZE);
// }
// return distToEnd + other.Value.Conveyor.DistanceFromEnd(SwapEnd(end));//Should Be space to inset, but needs to be serpate into 2 functons to prevent stgackoverflow
// }
public bool TryInsertAtEnd(ConveyorEnd end, IBeltItem itemStack)
{
var space = DistanceFromEnd(end);
if (space >= ITEMHALFSIZE)
{
InsertAtEndWithoutCheck(end, itemStack);
return true;
}
return false;
}
private void InsertAtEndWithoutCheck(ConveyorEnd end, IBeltItem item, float offset = 0)
{
ConveyorSlice itemPair = new(item, end switch { ConveyorEnd.Start => offset, ConveyorEnd.End => Length - offset, _ => throw new NotSupportedException() });
if (end == ConveyorEnd.Start)
{
_items.Insert(0, itemPair);
}
else
{
_items.Add(itemPair);
}
}
private (ItemConveyor Conveyor, ConveyorPort Port)? GetConveyorPortFacing(ConveyorPort port, Predicate<ConveyorPort>? predicate = default)
{
predicate ??= _ => true;
var pos = VoxelPosition + port.LocalOffset + port.Face.ToVector();
var others = GridRegistry.Get<ItemConveyor>(pos);
foreach (var item in others)
{
var otherPorts = item._ports.Where(otherPort => otherPort.Face.Reverse() == port.Face && predicate(otherPort));
GD.Print("ports "+string.Join(',',otherPorts.Select(i=>i.BeltT)));
if (otherPorts.Any())
{
return (item, otherPorts.First());
}
}
return null;
}
public Guid Id { get; set; }
[Export]
public Vector3I VoxelPosition { get; set; }
[Export]
public Direction VoxelRotation { get; set; } = Direction.Front;//test
public IEnumerable<Vector3I> Shape => [Vector3I.Zero];
private readonly AutoList<ConveyorSlice> _items = [];
public IAutoList<ConveyorSlice> Items => _items;
public struct ConveyorPort : IBeltSlotProfile// May chagne to record
{
public Vector3I LocalOffset;
public BeltT BeltT;
public Direction Face;
public TransferMode PullPush;
public PortAccess Direction;
public Func<IBeltItem, float, bool>? AcceptItemFunc;
public Func<IBeltItem, float, bool>? CanAcceptItemFunc;
public IMovementConveyor MovementConveyor;
public readonly Vector3I Position => LocalOffset;
public readonly int Width => 1;
public readonly PortAccess Access => Direction;
public Guid Id => throw new NotImplementedException();
public Vector3I VoxelPosition => LocalOffset;
public Direction VoxelRotation => throw new NotImplementedException();
public IEnumerable<Vector3I> Shape => [new Vector3I(0, 0, 0)];
// readonly Direction IBeltSlotProfile.Direction => Face;
[MemberNotNullWhen(true,nameof(CanAcceptItemFunc))]
[MemberNotNullWhen(true,nameof(AcceptItemFunc))]
public readonly bool CanAcceptItem(IBeltItem beltItem, LaneSpan laneSpan, float beltT = 0)
{
if (CanAcceptItemFunc is null || AcceptItemFunc is null || !Access.HasFlag(PortAccess.In) )//|| LaneSpan.Encapsulates(new(0, (ushort)Width), laneSpan))
{
return false;
}
return CanAcceptItemFunc(beltItem, beltT);
}
public readonly bool TryInsertItem(IBeltItem beltItem, LaneSpan laneSpan, float beltT = 0)
{
if (!CanAcceptItem(beltItem, laneSpan, beltT))
{
return false;
}
return AcceptItemFunc(beltItem, beltT);
}
}
public abstract record BeltT();
public record BeltTEnd(ConveyorEnd End) : BeltT();
public record BeltTOffset(float T) : BeltT();
// public enum PortAccess : byte
// {
// In,
// Out,
// InOut,
// BiDirectional = InOut
// }
// [Flags]
// public enum TransferMode : byte//Need Better Name
// {
// None = 0,
// Push = 1,
// Pull = 2,
// PushPull = Push | Pull
// }
public interface IBeltMovement
{
void AdvanceBelt(ItemConveyor conveyor, float delta);
void AdvanceBelt(IMovementConveyor conveyor, float delta);
// event ItemMoved? OnItemMoved;
// delegate void ItemMoved(
// IBeltItem item,
// ReadOnlySpan<ItemMovementSegment> segments,
// float deltaTime
// );
}
public sealed class IndividualMovement : IBeltMovement
{
public void AdvanceBelt(ItemConveyor conveyor, float delta)
{
if (!conveyor.AnyItems())
{
return;
}
var beltDirection = conveyor.GetBeltDirection();
if (beltDirection == BeltDirection.NotMoving)
{
return;
}
var towardStart = beltDirection == BeltDirection.TowardStart;
var next = conveyor.GetConveyorPortFacing(conveyor.GetOutputPort());
if (towardStart)
{
for (int i = 0; i < conveyor.ItemsCount; i++)
{
var possibleItemTravel = conveyor.GetAvailableTravelForFrontItem(next.HasValue);
var maxSpeed = Mathf.Min(conveyor.SpeedMagnitude * delta, possibleItemTravel.Value) * Mathf.Sign(conveyor.SignedSpeed);//Or possibly (int)BeltDirection, givn that they stgore the sign as part of the enum
if (maxSpeed == 0)
{
return;
}
var endT = Mathf.Clamp(conveyor._items[i].BeltT + maxSpeed, 0, conveyor.Length);
conveyor._items[i] = conveyor._items[i] with { BeltT = endT };
if (endT <= 0)
{
// segments.Add(new(next.Value.Conveyor, ));
if (next.Value.Port.TryInsertItem(conveyor._items[i].Item, LaneSpan.One))
{
GD.Print("Item Moved");
conveyor._items.Remove(conveyor._items[i]);
i--;
}
}
}
}
else
{
for (int i = conveyor.ItemsCount-1; i >=0 ; i--)
{
var possibleItemTravel = conveyor.GetAvailableTravelForFrontItem(next.HasValue);
var maxSpeed = Mathf.Min(conveyor.SpeedMagnitude * delta, possibleItemTravel.Value) * Mathf.Sign(conveyor.SignedSpeed);//Or possibly (int)BeltDirection, givn that they stgore the sign as part of the enum
if (maxSpeed == 0)
{
return;
}
var endT = Mathf.Clamp(conveyor._items[i].BeltT + maxSpeed, 0, conveyor.Length);
conveyor._items[i] = conveyor._items[i] with { BeltT = endT };
if (endT >= conveyor.Length)
{
// segments.Add(new(next.Value.Conveyor, ));
if (next.Value.Port.TryInsertItem(conveyor._items[i].Item, LaneSpan.One))
{
GD.Print("Item Moved");
conveyor._items.Remove(conveyor._items[i]);
i--;
}
}
}
}
}
public void AdvanceBelt(IMovementConveyor conveyor, float delta)
{
// GD.Print(conveyor.Items);
if (conveyor.GetBeltDirection() == BeltDirection.NotMoving)
{
return;
}
// GD.Print("hello");
var towardStart = conveyor.GetBeltDirection() == BeltDirection.TowardStart;
foreach (var itemRef in towardStart ? conveyor.EnumerateTowardStart() : conveyor.EnumerateTowardEnd())
{
float maxMove = conveyor.SpeedMagnitude * delta;
var space = conveyor.GetDistanceToNextItem(conveyor.GetBeltDirection(),itemRef.Value.BeltT,maxMove,itemRef.Value.LaneSpan);
// space -= ITEMSIZE;//AcountForSPacing
// if ()
// {
// continue;
// }
if (space is ItemBeltObstacle itemOb && itemOb.Distance <= 0)
{
continue;
}
float amountToMove = Mathf.Min(space.Distance, maxMove);
itemRef.Replace(itemRef.Value with { BeltT = itemRef.Value.BeltT + amountToMove * Mathf.Sign(conveyor.SignedSpeed)});
// GD.PrintS(conveyor.Items.Count,space,amountToMove," "+ itemRef.BeltT,itemRef.Item,conveyor.GetBeltDirection(),towardStart);
if (towardStart ? itemRef.Value.BeltT <= 0 : itemRef.Value.BeltT >= 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(itemRef.Value.Item, space is PortBeltObstacle portO ? portO.LaneSpan : itemRef.Value.LaneSpan, 0))
{
// GD.Print(space is PortBeltObstacle port?port.LaneSpan:itemRef.Value.LaneSpan);
// GD.PrintS(itemRef.Value.LaneSpan,itemRef.Value.LaneSpan);
// GD.Print(itemRef.Item);
itemRef.Remove();
}
}
}
}
}
public sealed class StrictMovement : IBeltMovement
{
// public event IBeltMovement.ItemMoved? OnItemMoved;
public void AdvanceBelt(ItemConveyor conveyor, float delta)
{
if (!conveyor.AnyItems())
{
return;
}
var beltDirection = conveyor.GetBeltDirection();
if (beltDirection == BeltDirection.NotMoving)
{
return;
}
var towardStart = beltDirection == BeltDirection.TowardStart;
// GD.Print("Thing: " + Speed + " "+ conveyor.GetConveyorPortFacing(conveyor.GetOutputPort()));
// var moveItemsIntoNextBelt = false;
var next = conveyor.GetConveyorPortFacing(conveyor.GetOutputPort());
var possibleItemTravel = conveyor.GetAvailableTravelForFrontItem(next.HasValue);
// return;
if (!possibleItemTravel.HasValue)
{
//There is no items, this should not be called
throw new NotSupportedException("This should not e possible if there is no items or the belt is not moving, it should have returend before");
}
// if (next.HasValue && next.Value.Port.BeltT is BeltTEnd beltTEnd && (next.Value.Port.Direction == PortAccess.In || next.Value.Port.Direction == PortAccess.BiDirectional))
// {
// possibleItemTravel += next.Value.Conveyor.DistanceFromEnd(beltTEnd.End);
// moveItemsIntoNextBelt = true;
// }
// var itemPair = conveyor.GetFirstItem();
// var distToEnd = conveyor.GetAvailableTravel(conveyor.ItemsCount - 1, beltDirection);//(Speed < 0 ? ConveyorEnd.Start : ConveyorEnd.End);
var maxSpeed = Mathf.Min(conveyor.SpeedMagnitude * delta, possibleItemTravel.Value) * Mathf.Sign(conveyor.SignedSpeed);//Or possibly (int)BeltDirection, givn that they stgore the sign as part of the enum
if (maxSpeed == 0)
{
// var fI = conveyor.GetAvailableTravelForFrontItem();
// if (fI.HasValue && fI.Value <= 0 && next.Value.Port.TryInsertItem(conveyor.GetFirstItem().Value.Item, LaneSpan.One))
// {
// GD.Print("Item Moved");
// conveyor._items.Remove(conveyor.GetFirstItem().Value);
// }
return;
}
List<ConveyorSlice> toMove = [];
// Dictionary<IBeltItem, List<ItemMovementSegment>> moves = [];
for (int i = 0; i < conveyor.ItemsCount; i++)
{
// List<ItemMovementSegment> segments = [];
// var startT = conveyor._items[i].BeltT;
var endT = Mathf.Clamp(conveyor._items[i].BeltT + maxSpeed, 0, conveyor.Length);
conveyor._items[i] = conveyor._items[i] with { BeltT = endT };
// segments.Add(new(conveyor, startT, towardStart? Mathf.Max(endT, 0) : Mathf.Min(endT, conveyor.Length),Mathf.Abs(maxSpeed)));
if (towardStart ? (endT <= 0) : endT >= conveyor.Length)
{
toMove.Add(conveyor._items[i]);
// segments.Add(new(next.Value.Conveyor, ));
}
// moves.Add(conveyor._items[i].Item, segments);
GD.Print($"Item: {conveyor._items[i].Item}, Position:{endT}");
}
// conveyor._items.ForEach((item) => item = item = Mathf.Clamp(item.Position + maxSpeed, 0, Length));
// var list = _items.Where(item=> Speed>=0?item.BeltT >= Length:item.BeltT <= 0).ToList();
// GD.Print(list.Count);
if (next.HasValue)
{
foreach (var item in toMove)
{
GD.Print($"Item: {item.Item}, Position:{item.BeltT} -----------------");
//TEST
// var v = GetConveyorPortFacing(GetOutputPort());
// if (v.HasValue)//Should be true if items are > Length
// {
// GD.PrintS(v.Value.Port.LocalOffset,v.Value.Port.BeltT,v.Value.Port.Direction,v.Value.Port.PullPush,v.Value.Port.Face);
// var end = (next.Value.Port.BeltT as BeltTEnd).End;
// next.Value.Conveyor.InsertAtEndWithoutCheck(end, item.Item);
if (next.Value.Port.TryInsertItem(item.Item, item.LaneSpan))
{
GD.Print("Item Moved");
conveyor._items.Remove(item);
}
}
}
}
public void AdvanceBelt(IMovementConveyor conveyor, float delta) => throw new NotImplementedException();
}
}
public struct ConveyorSlice(IBeltItem item, float beltT = 0)
{
public IBeltItem Item = item;
public float BeltT = beltT;
public LaneSpan LaneSpan = LaneSpan.One;
public override string ToString() => $"{Item}, {BeltT}, {LaneSpan}";
}
public readonly struct LaneSpan : IEquatable<LaneSpan>
{
public readonly ushort Start;
public readonly ushort End;
public readonly int Width => End - Start;
public static readonly LaneSpan One = new(0, 1);
public static readonly LaneSpan Zero = new(0, 0);
public LaneSpan(ushort start, ushort end)
{
Start = start;
End = end;
}
// [MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static bool OverLaps(LaneSpan a, LaneSpan b) => a.Start < b.End && b.Start < a.End;
// [MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static LaneSpan Shifted(LaneSpan a, int amount) => checked(new LaneSpan((ushort)(a.Start + amount), (ushort)(a.End + amount)));
public static bool Encapsulates(LaneSpan a, LaneSpan b) => b.Start>=a.Start && b.End<= b.End;
public override string ToString() => $"(Start:{Start}, End:{End})";
public bool Equals(LaneSpan other) => Start == other.Start && End == other.End;
public static bool operator ==(LaneSpan a, LaneSpan b) => a.Equals(b);
public static bool operator !=(LaneSpan a, LaneSpan b) => !a.Equals(b);
}
public static class Extersions
{
public static ItemConveyor.BeltDirection DirectionTo(this ItemConveyor.ConveyorEnd end) => end switch
{
ItemConveyor.ConveyorEnd.Start => ItemConveyor.BeltDirection.TowardStart,
ItemConveyor.ConveyorEnd.End => ItemConveyor.BeltDirection.TowardEnd,
_ => throw new NotImplementedException(),
};
}

View File

@@ -0,0 +1 @@
uid://cx35jfqkjnou8

View File

@@ -0,0 +1,131 @@
namespace ChickenGameTest;
using System;
using System.Collections.Generic;
using System.Linq;
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
[Meta(typeof(IAutoNode))]
public partial class SlotComponentNode
: Node3D , IEquipmentComponent
{
public override void _Notification(int what) => this.Notify(what);
[Export] public SlotDirection Direction;
[Export] public Vector3I LocalCellOffset;
[Dependency] protected IVoxelGridQuery<LayeredEquipment> Grid => this.DependOn<IVoxelGridQuery<LayeredEquipment>>();
[Dependency] protected IEquipmentContext EquipmentContext =>this.DependOn<IEquipmentContext>();
[Dependency] protected IEquipmentComponentRegistry ComponentRegistry => this.DependOn<IEquipmentComponentRegistry>();
public EquipmentId EquipmentId => EquipmentContext.GetEquipmentId();
// SO instead of using an hidden list of compeonts on the equimpent, have the equpment provide an reggistery that compnents depend on to register themselves to, and allow for muitple of the same type.
public void OnResolved()
{
GD.PrintS(EquipmentContext, Grid);
EquipmentContext.GetEquipment().Tick += Tick;
ComponentRegistry.Register(this);
GD.Print("Added Tick Listener");
}
private void Tick()
{
Grid.GetVoxel(EquipmentContext.GetEquipment().GridPos + LocalCellOffset/*TODO Acount for Direction*/).IfAny(e =>
{
var slots = e.SelectMany(ee => ee.GetChildren().OfType<SlotComponentNode>().Where(_=> true/*Where slot faces this one and is not output*/)).ToList();
// slots.First().AcceptItem(new TestItem());//For testing accuming adding item, should probly sort and cascade so progatrion stops when out of items, but should only be one result, as each componet is one face
});
// GD.Print("Tryied moveing items test");
}
public bool AcceptItem(IBeltItem itemStack)
{
return true;
}
public override void _ExitTree()
{
base._ExitTree();
EquipmentContext.GetEquipment().Tick -= Tick;
ComponentRegistry.Unregister(this);
}
// public Vector3I GetSlotCell() {
// return Equipment.GetOccupiedCell() + LocalCellOffset;
// }
// public Vector3 GetSlotWorldPosition() {
// return Grid.CellToWorld(GetSlotCell());
// }
}
public interface IEquipmentComponentRegistry {
void Register<T>(T component)
where T : class, IEquipmentComponent;
void Unregister<T>(T component)
where T : class, IEquipmentComponent;
T? Get<T>(EquipmentId id)
where T : class, IEquipmentComponent;
IReadOnlyList<T> GetAll<T>(EquipmentId id)
where T : class, IEquipmentComponent;
}public sealed class EquipmentComponentRegistry
: IEquipmentComponentRegistry {
private readonly Dictionary<
EquipmentId,
Dictionary<Type, List<object>>
> _map = new();
public void Register<T>(T component)
where T : class, IEquipmentComponent {
GD.Print(component);
if (!_map.TryGetValue(component.EquipmentId, out var types)) {
types = new();
_map[component.EquipmentId] = types;
}
var type = typeof(T);
if (!types.TryGetValue(type, out var list)) {
list = new();
types[type] = list;
}
list.Add(component);
}
public T? Get<T>(EquipmentId id)
where T : class, IEquipmentComponent {
if (_map.TryGetValue(id, out var types) &&
types.TryGetValue(typeof(T), out var list))
return list[0] as T;
return null;
}
public IReadOnlyList<T> GetAll<T>(EquipmentId id)
where T : class, IEquipmentComponent {
if (_map.TryGetValue(id, out var types) &&
types.TryGetValue(typeof(T), out var list))
return list.Cast<T>().ToList();
return Array.Empty<T>();
}
public void Unregister<T>(T component)
where T : class, IEquipmentComponent {
if (_map.TryGetValue(component.EquipmentId, out var types) &&
types.TryGetValue(typeof(T), out var list))
list.Remove(component);
}
}
public interface IEquipmentComponent {
EquipmentId EquipmentId { get; }
}

View File

@@ -0,0 +1 @@
uid://drsqsmj0bn1ob

250
src/VoxelGrid/SlotFace.cs Normal file
View File

@@ -0,0 +1,250 @@
namespace ChickenGameTest;
using Chickensoft.GodotNodeInterfaces;
using Chickensoft.Introspection;
using Chickensoft.AutoInject;
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using ChickenGameTest;
public interface ISlotFace : IVoxelNode // INode3D,
{
ISlotFace GetConnectingFace();
}
[Tool]
[Meta(typeof(IAutoNode))]
public partial class SlotFace : Node3D, ISlotFace
{
public Guid Id { get; set; }
public IEnumerable<Vector3I> Shape => [Vector3I.Zero];
[Export] public Vector3I VoxelPosition { get; set; }
[Export] public Direction VoxelRotation { get; set; }
[Export] public bool ShowDebug { get; set; } = false;
public override void _Notification(int what) => this.Notify(what);
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
public void OnResolved()
{
GridRegistry.Register(this);
GD.Print("Regestered");
}
public ISlotFace GetConnectingFace()//TODO there could be tecnaly muiple slots shareing a face, and should be acounted for
{
var nodes = GridRegistry.Get<ISlotFace>(VoxelPosition + VoxelRotation.ToVector());//TODO Acoount for rotation to get slot direction
// GD.Print(nodes.Count());
foreach (var item in nodes)
{
if (item.VoxelRotation.Reverse() == VoxelRotation)
{
return item;
}
}
return null;
}
private static Vector3[] _Square = [new(-.5f, .5f, -.5f), new(.5f, .5f, -.5f), new(.5f, -.5f, -.5f), new(-.5f, -.5f, -.5f), new(-.5f, .5f, -.5f)];
public override void _Process(double delta)
{
if (ShowDebug)
{
ShowDebugFace();
}
}
private void ShowDebugFace()
{
var color = Colors.Red;
var pos = ToGlobal(new Vector3(0, 0, -.5f));
if (!Engine.IsEditorHint())
{
color = GetConnectingFace() is null ? Colors.Red : Colors.Green;
}
// var text = $"SlotFace{Name}";
// GD.PrintS(color);
DebugDraw3D.DrawLinePath([.. _Square.Select(ToGlobal)],color);
// DebugDraw3D.DrawText(pos, text, 16);
}
}
public interface IVoxelGridRegistry
{
[Obsolete]
void Register<T>(T voxelNode) where T : IVoxelNode;
void Register<T>(T voxelNode, params Vector3I[] positions);
void UnRegister<T>(T voxelNode) where T : IVoxelNode=> UnRegister(voxelNode.Id);
void UnRegister<T>(T voxelNode, params Vector3I[] positions);
void UnRegister(Guid id);
IEnumerable<T> Get<T>(Vector3I voxelPos);
IEnumerable<T> Get<T>(params Vector3I[] voxelPos);
}
public sealed class VoxelRegistry : IVoxelGridRegistry
{
private Dictionary<Vector3I, ICollection<object>> _data;
public VoxelRegistry()
{
_data = new();
}
public IEnumerable<T> Get<T>(Vector3I voxelPos)
{
if (!_data.TryGetValue(voxelPos, out var entrys))
{
yield break;
}
foreach (var item in entrys.OfType<T>())
{
yield return item;
}
}
public IEnumerable<T> Get<T>(params Vector3I[] voxelPos)
{
foreach (var item in voxelPos)
{
foreach (var item2 in Get<T>(item))
{
yield return item2;
}
}
}
public void Register<T>(T voxelNode) where T : IVoxelNode
{
void register(Vector3I pos, T point)
{
if (!_data.TryGetValue(pos, out var entrys))
{
entrys = [];
_data[pos] = entrys;
}
entrys.Add(point);
}
voxelNode.Shape.ToList().ForEach(
item =>
{
register(item + voxelNode.VoxelPosition, voxelNode);//TODO Acount For Rotation
}
);
}
public void Register<T>(T voxelNode, params Vector3I[] positions)
{
void register(Vector3I pos, T point)
{
if (!_data.TryGetValue(pos, out var entrys))
{
entrys = [];
_data[pos] = entrys;
}
entrys.Add(point);
}
foreach (var pos in positions)
{
if (positions.Length>1){
GD.Print("hhhhhhhhh ",pos);}
register(pos, voxelNode);//TODO Acount For Rotation
}
}
public void UnRegister(Guid id) => throw new NotImplementedException();
public void UnRegister<T>(T voxelNode, params Vector3I[] positions)
{
void unRegister(Vector3I pos, T point)
{
if (!_data.TryGetValue(pos, out var entrys))
{
return;
}
entrys.Remove(point);
}
foreach (var item in positions)
{
unRegister(item, voxelNode);
}
}
}
public interface IVoxelNode
{
Guid Id { get; }
Vector3I VoxelPosition { get; }
Direction VoxelRotation { get; } //TODO replace with struct/record for static rotation typeing
IEnumerable<Vector3I> Shape { get; }//TOPO make SHape able to account for occpancy/ partial filled voxels.
}
public enum Direction
{
Up, Down, Left, Right, Front, Back
}
public static class DirectionExtession
{
public static Vector3I ToVector(this Direction direction) => direction switch
{
Direction.Up => Vector3I.Up,
Direction.Down => Vector3I.Down,
Direction.Left => Vector3I.Left,
Direction.Right => Vector3I.Right,
Direction.Front => Vector3I.Forward,
Direction.Back => Vector3I.Back,
_ => throw new NotSupportedException($"{nameof(direction)} does not support value {direction}")
};
public static Direction Reverse(this Direction direction) => direction switch
{
Direction.Up => Direction.Down,
Direction.Down => Direction.Up,
Direction.Left => Direction.Right,
Direction.Right => Direction.Left,
Direction.Front => Direction.Back,
Direction.Back => Direction.Front,
_ => throw new NotSupportedException($"{nameof(direction)} does not support value {direction}")
};
public static Direction RotateClockWise(this Direction direction) => direction switch
{
Direction.Up => Direction.Up,
Direction.Down => Direction.Down,
Direction.Left => Direction.Back,
Direction.Right => Direction.Front,
Direction.Front => Direction.Right,
Direction.Back => Direction.Left,
_ => throw new NotSupportedException($"{nameof(direction)} does not support value {direction}")
};
public static Direction RotateCounterClockWise(this Direction direction) => direction switch
{
Direction.Up => Direction.Up,
Direction.Down => Direction.Down,
Direction.Right => Direction.Front,
Direction.Left => Direction.Back,
Direction.Back => Direction.Right,
Direction.Front => Direction.Left,
_ => throw new NotSupportedException($"{nameof(direction)} does not support value {direction}")
};
}
public interface IStorage<T> where T : class
{
StorageDefinition Definition { get; }
IEnumerable<T> GetAllItems();
void Remove(T item);
bool TryInsert(T item);
record StorageDefinition(string Name, ItemType Type);
}
public record ItemType(string Name);
public partial class ItemStoragePool : Node, IStorage<IBeltItem>
{
[Export] public int MaxAmount { get; set; } = 5;
public IStorage<IBeltItem>.StorageDefinition Definition { get; set; } = default!;
private readonly List<IBeltItem> _items = [];
public IEnumerable<IBeltItem> GetAllItems() => _items;
public bool TryInsert(IBeltItem item)
{
if (_items.Count >= MaxAmount)
{
return false;
}
_items.Add(item);
return true;
}
public void Remove(IBeltItem item) => _items.Remove(item);
}

View File

@@ -0,0 +1 @@
uid://dao8u7hn0yadp

View File

@@ -0,0 +1,711 @@
namespace ChickenGameTest;
using Chickensoft.Introspection;
using Chickensoft.AutoInject;
using Godot;
using System;
using System.Collections.Generic;
using System.Linq;
using Chickensoft.Sync.Primitives;
using SJK.Functional;
public class Sorted1DList<T>
{
private readonly AutoList<T> _items = [];
#if DEBUG
private readonly AutoList<T>.Binding _binding;
#endif
private readonly Func<T, float> _getPosition;
public Sorted1DList(Func<T, float> getPos)
{
_getPosition = getPos;
// return;
#if DEBUG
_binding = _items.Bind();
_binding.OnUpdate((a, b, c) =>
{
// GD.PrintS("++++++++++++++++",a,b,c);
});
// return;
_binding.OnModify(() =>
{
for (int i = 1; i < _items.Count; i++)
{
if (_getPosition(_items[i - 1]) >= _getPosition(_items[i]))
{
throw new InvalidOperationException(
$"AutoList not sorted at index {i - 1} → {i}. ({_getPosition(_items[i-1])},{_getPosition(_items[i])})");
}
}
});
#endif
}
public int Count => _items.Count;
public IAutoList<T> Items => _items;
public IEnumerable<ItemHandle> EnumerateTowardEnd(int? startIndex = null)
{
if (startIndex.HasValue && startIndex >= Count)
{
throw new IndexOutOfRangeException($"{nameof(startIndex)}:{startIndex} can not be greater then or equal to {nameof(Count)}:{Count}");
}
for (int i = Mathf.Max(0,startIndex ?? 0); i < _items.Count; i++)
{
bool removed = false;
yield return new(_items[i],
() =>
{
if (removed)
{
throw new NotSupportedException($"{nameof(ItemHandle.Remove)} can only be called once per item.");
}
removed = true;
_items.RemoveAt(i);
i--;
},
(replaced) =>
{
if (removed)
{
throw new NotSupportedException("Can not Replace an Item after Removing it");
}
_items[i] = replaced;
}
);
}
}
public IEnumerable<ItemHandle> EnumerateTowardStart(int? startIndex = null)
{
if (startIndex.HasValue && startIndex < 0)
{
throw new IndexOutOfRangeException($"{nameof(startIndex)}:{startIndex} can not be less than zero");
}
for (int i = Math.Min(startIndex ?? (_items.Count - 1), _items.Count -1); i>=0 ; i--)
{
bool removed = false;
yield return new(_items[i],
() =>
{
if (removed)
{
throw new NotSupportedException($"{nameof(ItemHandle.Remove)} can only be called once per item.");
}
removed = true;
_items.RemoveAt(i);
// i--;
},
(replaced) =>
{
if (removed)
{
throw new NotSupportedException("Can not Replace an Item after Removing it");
}
if (i-1>=0 && _getPosition(_items[i-1]) >= _getPosition(replaced))
{
throw new Exception();
}
if (i+1<Count && _getPosition(_items[i+1]) <= _getPosition(replaced))
{
throw new Exception();
}
_items[i] = replaced;
}
);
}
}
public void Insert(T item)
{
float pos = _getPosition(item);
if (_items.Count == 0 || pos >= _getPosition(_items[^1]))
{
_items.Add(item);
return;
}
if (pos <=_getPosition(_items[0]))
{
_items.Insert(0, item);
return;
}
var index = LowerBound(pos);
_items.Insert(index, item);
}
private int LowerBound(float pos)
{
int lo = 0;
int hi = _items.Count;
while (lo < hi)
{
int mid = (lo + hi) >> 1;
if (_getPosition(_items[mid]) < pos)
{
lo = mid + 1;
}
else
{
hi = mid;
}
}
return lo;
}
public (Option<T> lower, Option<T> upper) GetNeighbors(float pos)
{
int index = LowerBound(pos);
var lower = index > 0 ? Option<T>.Some(_items[index - 1]) : Option<T>.None;
var upper = index < _items.Count ? Option<T>.Some(_items[index]) : Option<T>.None;
return (lower, upper);
}
public readonly struct ItemHandle
{
private readonly Action _remove;
private readonly Action<T> _replace;
public T Value { get; }
public ItemHandle(T value, Action remove, Action<T> replace)
{
Value = value;
_remove = remove;
_replace = replace;
}
public void Remove() => _remove();
public void Replace(T slice) => _replace(slice);
}
}
[Meta(typeof(IAutoNode))][Tool]
public partial class TestItemConveyor : Node, IMovementConveyor
{
public override void _Notification(int what) => this.Notify(what);
[Dependency] public ItemConveyor.IBeltMovement MovementSystem => this.DependOn<ItemConveyor.IBeltMovement>(() => new ItemConveyor.IndividualMovement());
[Dependency] public IVoxelGridRegistry GridRegistry => this.DependOn<IVoxelGridRegistry>();
// private readonly AutoList<ConveyorSlice> _items = [];
// public IAutoList<ConveyorSlice> Items => _items;
public readonly Sorted1DList<ConveyorSlice> Items = new(pos => pos.BeltT);
// private AutoList<ConveyorSlice>.Binding _itemsBinding;
public IBeltPort StartPort { get; set; }
// public IBeltSlotProfile StartPort => new ItemConveyor.ConveyorPort() {
// Face = Direction.Back,
// MovementConveyor = this,
// LocalOffset = Position,
// BeltT = new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.Start),
// PullPush = TransferMode.PushPull ,
// Direction = PortAccess.BiDirectional,
// AcceptItemFunc = (item,offset)=>{ _items.Insert(0,new ConveyorSlice(item,offset)); return true;},
// CanAcceptItemFunc = (item, offset) =>
// {
// return .25f <= GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, 0, 5f, LaneSpan.One);
// }
// };
public IBeltPort EndPort { get; set; }
// public IBeltSlotProfile EndPort => new ItemConveyor.ConveyorPort() {
// Face = Direction.Front,
// MovementConveyor = this,
// Direction = PortAccess.BiDirectional,
// LocalOffset = Position,
// BeltT = new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.End),
// PullPush = TransferMode.PushPull,
// AcceptItemFunc = (item,offset)=>{ _items.Add(new ConveyorSlice(item,Length-offset)); return true;},
// CanAcceptItemFunc = (item,offset)=> .25f >= GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardStart,Length,.5f,LaneSpan.One)
// };
public IList<IBeltPort> OtherPorts = [];
// [Export] public Vector3I Position { get; set; } = default!;
//Speed per unit time
private readonly AutoValue<float> _speed = new(1);
public IAutoValue<float> SpeedValue => _speed;
public float SignedSpeed
{
get => _speed.Value;
set => _speed.Value = value;
}
public float SpeedMagnitude
{
get => Mathf.Abs(_speed.Value);
set => _speed.Value = Mathf.Abs(value) * Mathf.Sign(_speed.Value);
}
public bool IsReversed { get => _speed.Value < 0; set => _speed.Value = SpeedMagnitude * (value ? -1 : 1); }
// public IList<ConveyorSlice> Items => _items;
public float Length { get; set; } = 1;
// public IEnumerable<ConveyorSlice> EnumerateTowardEnd() => _items;
// public IEnumerable<ConveyorSlice> EnumerateTowardStart() => _items.Reverse();
public ItemConveyor.BeltDirection GetBeltDirection() => IsReversed ? ItemConveyor.BeltDirection.TowardStart : ItemConveyor.BeltDirection.TowardEnd;
public ItemConveyor.IBeltMovement GetMovementPolicy() => MovementSystem;
public IEnumerable<IBeltPort> GetPorts() => [StartPort, EndPort, .. OtherPorts];
// public IEnumerable<IBeltSlotProfile> GetPorts() => [StartPort, EndPort,
// new ItemConveyor.ConveyorPort(){
// BeltT = new ItemConveyor.BeltTOffset(.5f),
// Face = Direction.Right,
// Direction = PortAccess.InOut,
// PullPush = TransferMode.Passive,
// MovementConveyor = this,
// LocalOffset = Position,
// },
// new ItemConveyor.ConveyorPort(){
// BeltT = new ItemConveyor.BeltTOffset(.5f),
// Face = Direction.Left,
// Direction = PortAccess.InOut,
// PullPush = TransferMode.Passive,
// MovementConveyor = this,
// LocalOffset = Position,
// },
// new ItemConveyor.ConveyorPort(){
// BeltT = new ItemConveyor.BeltTOffset(.5f),
// Face = Direction.Up,
// Direction = PortAccess.In,
// PullPush = TransferMode.Passive,
// MovementConveyor = this,
// LocalOffset = Position,
// }
// ];Path3D.Curve.SampleBakedWithRotation(b.BeltT).Basis.X*b.Item.Width
public void OnResolved()
{
if (Engine.IsEditorHint())
{
return;
}
var timer = new Timer() { WaitTime = .05f, Autostart = true };
AddChild(timer);
timer.Timeout += () => MovementSystem.AdvanceBelt(this, (float)timer.WaitTime);
// Items.Add(new ConveyorSlice(new TestItem(), 0));
// StartPort = new ConveyorPort(this,
// new BeltPortProfile(Position, Direction.Back, 1, PortAccess.BiDirectional),
// new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.Start),
// (item, offset) =>
// {
// var obstacle = GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, 0, offset, LaneSpan.One);
// return ItemConveyor.ITEMSIZE < obstacle.DistanceToCenter - (obstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// },
// (item, offset) => { _items.Insert(0, new(item, offset)); return true;});
// EndPort = new ConveyorPort(this,
// new BeltPortProfile(Position, Direction.Front, 1, PortAccess.BiDirectional),
// new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.End),
// (item, offset) =>
// {
// var obstacle = GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, Length, offset, LaneSpan.One);
// return ItemConveyor.ITEMSIZE < obstacle.DistanceToCenter - (obstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// // return ItemConveyor.ITEMSIZE < GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardStart, Length, SpeedMagnitude, LaneSpan.One);
// },
// (item, offset) => { _items.Add(new(item, offset)); return true;});
if (StartPort is null || EndPort is null)
{
throw new Exception();
}
foreach (var item in GetPorts())
{
var points = item.Points();
GridRegistry.Register(item, [.. points]);
}
StartPort.TryInsert(new TestItem(), LaneSpan.Shifted(LaneSpan.One,0), 0);
// StartPort.TryInsert(new TestItem(), LaneSpan.Sh fted(LaneSpan.One,1), 0);
// Items.Insert(new ConveyorSlice(new TestItem(), 0));
// _items.Add(new (new TestItem()));
}
public override void _Ready()
{
// _itemsBinding = _items.Bind();
// _itemsBinding.OnModify(() =>
// {
// });
// OnResolved();
base._Ready();
//test
}
//ChatGPT Assisted
public BeltObstacle GetDistanceToNextItem(
ItemConveyor.BeltDirection beltDirection,
float itemBeltT,
float maxDistToCheck,
LaneSpan laneSpan,
HashSet<IMovementConveyor>? visited = null)
{
if (beltDirection == ItemConveyor.BeltDirection.NotMoving)
{
throw new NotSupportedException();
}
visited ??= [];
if (!visited.Add(this))
{
return new BeltObstacle(0);
}
bool towardStart = beltDirection == ItemConveyor.BeltDirection.TowardStart;
// 1⃣ Try to find next item on this conveyor
var nextItem = FindNextLocalItem(towardStart, itemBeltT, laneSpan);
// GD.Print(nextItem);
if (nextItem.HasValue(out var item))
{
return new ItemBeltObstacle(MathF.Abs(itemBeltT - item.BeltT) - ItemConveyor.ITEMSIZE, item, laneSpan);
// return MathF.Abs(itemBeltT - item.BeltT);//TODO UseLaneSpan inadditon
}
// 2⃣ No local item → try crossing into adjacent conveyor
return GetDistanceAcrossPort(
towardStart,
itemBeltT,
maxDistToCheck,
laneSpan,
visited
);
}
//ChatGPT Assisted
private IOption<ConveyorSlice> FindNextLocalItem(
bool towardStart,
float itemBeltT,
LaneSpan laneSpan)
{
var sequence = towardStart
? EnumerateTowardStart()
: EnumerateTowardEnd();
return sequence
.Where(i => towardStart ? i.Value.BeltT < itemBeltT : i.Value.BeltT > itemBeltT)
.Where(i => LaneSpan.OverLaps(i.Value.LaneSpan, laneSpan))
.Select(item => item.Value)
.FirstOrNone();
}
//ChatGPT Assisted
private BeltObstacle GetDistanceAcrossPort(
bool towardStart,
float itemBeltT,
float maxDistToCheck,
LaneSpan laneSpan,
HashSet<IMovementConveyor> visited)
{
var port = towardStart ? StartPort : EndPort;
var boundaryT = towardStart ? 0f : Length;
var distanceToBoundary = MathF.Abs(itemBeltT - boundaryT);
// 🚫 Boundary already exceeds budget
// if (distanceToBoundary >= maxDistToCheck)
// {
// return maxDistToCheck;
// }
var neighborPort = FindFacingConveyorPort(port);
if (!neighborPort.HasValue(out var conveyorPort))
{
var o = GetPortFacing(port);
if (o.HasValue(out var otherport))
{
var mappedLane2 = MapLaneOrFail(port, otherport, laneSpan);
if (mappedLane2.HasValue(out var lane2))
{
return new PortBeltObstacle(distanceToBoundary,lane2,otherport);
// return new BeltObstacle(ObstacleKind.Boundary,distanceToBoundary+1,lane1,this,null, null);//TODO THIS SHOULD GET SPACE IN PORT SO ITEMS KNOW IF THEY CAN FIT
}
return new BoundaryBeltObstacle(distanceToBoundary);
// return new BeltObstacle(ObstacleKind.Boundary,distanceToBoundary,laneSpan,this,null, null);//TODO THIS SHOULD GET SPACE IN PORT SO ITEMS KNOW IF THEY CAN FIT
}
return new BoundaryBeltObstacle(distanceToBoundary);
}
if (conveyorPort.BeltT is not ItemConveyor.BeltTEnd end)
{
return new BoundaryBeltObstacle(distanceToBoundary);
}
var mappedLane = MapLaneOrFail(port, conveyorPort, laneSpan);
if (!mappedLane.HasValue(out var lane))
{
return new BoundaryBeltObstacle(distanceToBoundary);
}
var nextDirection = DirectionAwayFromEnd(end.End);
var startT = end.End == ItemConveyor.ConveyorEnd.Start
? 0f
: conveyorPort.Conveyor.Length;
// 🔻 Remaining budget after reaching boundary
// var remainingDist = maxDistToCheck - distanceToBoundary;
// if (remainingDist <= 0f)
// {
// return maxDistToCheck;
// }
var recursiveDistance =
conveyorPort.Conveyor.GetDistanceToNextItem(
nextDirection,
startT,
maxDistToCheck,
lane,
visited
);
return recursiveDistance with { Distance = recursiveDistance.Distance + distanceToBoundary };
// return new BeltObstacle(recursiveDistance.Kind, recursiveDistance.DistanceToCenter + distanceToBoundary, recursiveDistance.LaneSpan, recursiveDistance.Conveyor, recursiveDistance.Item,null);
// return distanceToBoundary + recursiveDistance;
}
private IOption<ConveyorPort> FindFacingConveyorPort(
IBeltPort slot)
{
var targetpos = slot.Profile.LocalOffset.TransformDirection(Vector3I.Forward);
// var targetPos = port.Profile.Position + port.Profile.Face.ToVector();
return GridRegistry
.Get<IBeltPort>(slot.Profile.LocalOffset.LocalToWorld(Vector3I.Forward))
.Where(port => port is ConveyorPort)
.Where(port => port.Profile.LocalOffset.TransformDirection(Vector3I.Forward) == slot.Profile.LocalOffset.TransformDirection(Vector3I.Back))
.FirstOrNone()
.Bind(p => p is ConveyorPort cp
? cp.ToOption()
: None<ConveyorPort>.Of());
}
public IOption<IBeltPort> GetPortFacing(IBeltPort slot)
{
var toCheck = new List<Vector3I>();
for (int i = 0; i < slot.Profile.Width; i++)
{
toCheck.Add(slot.Profile.LocalOffset.LocalToWorld(Vector3I.Forward));
}
return GridRegistry
.Get<IBeltPort>([.. toCheck])
.Where(port => port.Profile.LocalOffset.TransformDirection(Vector3I.Forward) == slot.Profile.LocalOffset.TransformDirection(Vector3I.Back))
.FirstOrNone();
}
private IOption<LaneSpan> MapLaneOrFail(
IBeltPort from,
IBeltPort to,
LaneSpan incoming)
{
// return None<LaneSpan>.Of();
bool mirror = false;
// full mapping of the source port
var mapped = from.MapLaneSpanToFacingPort(to);
if (mapped == LaneSpan.Zero)
return LaneSpan.Zero.ToOption();
int sourceWidth = from.Profile.Width;
int targetWidth = mapped.Width;
int startOffset = incoming.Start; // start relative to source port
int spanLength = incoming.Width;
int newStart;
if (mirror)
{
// mirrored: right side of incoming aligns with right side of mapped
newStart = mapped.End - (startOffset + spanLength);
}
else
{
// normal: left side of incoming aligns with left side of mapped
newStart = mapped.Start + startOffset;
}
int newEnd = newStart + spanLength;
// clamp to mapped range
newStart = Math.Max(mapped.Start, newStart);
newEnd = Math.Min(mapped.End, newEnd);
GD.Print(incoming, new LaneSpan((ushort)newStart, (ushort)newEnd));
if (newStart >= newEnd)
{
GD.Print("none");
return None<LaneSpan>.Of();
}
return new LaneSpan((ushort)newStart, (ushort)newEnd).ToOption();
}
private ItemConveyor.BeltDirection DirectionAwayFromEnd(ItemConveyor.ConveyorEnd conveyorEnd) => conveyorEnd switch {ItemConveyor.ConveyorEnd.Start=>ItemConveyor.BeltDirection.TowardEnd,ItemConveyor.ConveyorEnd.End=>ItemConveyor.BeltDirection.TowardStart};
public IEnumerable<Sorted1DList<ConveyorSlice>.ItemHandle> EnumerateTowardEnd() => Items.EnumerateTowardEnd();
public IEnumerable<Sorted1DList<ConveyorSlice>.ItemHandle> EnumerateTowardStart() => Items.EnumerateTowardStart();
private static Vector3[] _Square = [new(-.5f, .5f, -.5f), new(.5f, .5f, -.5f), new(.5f, -.5f, -.5f), new(-.5f, -.5f, -.5f), new(-.5f, .5f, -.5f)];
public override void _Process(double delta)
{
if (StartPort is null || EndPort is null)
{
return;
}
foreach (var item in GetPorts())
{
for (int ii = 0; ii < item.Profile.Width; ii++)
{
DebugDraw3D.DrawLinePath(_Square.Select(i => item.Profile.LocalOffset.ToGodot().TranslatedLocal(i).TranslatedLocal(new Vector3(ii,0,0)).Origin).ToArray(),(!Engine.IsEditorHint())&&GetPortFacing(item).HasValue()?Colors.Green:Colors.Red);
}
// DebugDraw3D.DrawArrow(item.Profile.LocalOffset.Origin, item.Profile.LocalOffset.LocalToWorld(item.Profile.Face.ToVector()),(!Engine.IsEditorHint())&&GetPortFacing(item).HasValue()?Colors.Green:Colors.Red);
}
}
public ConveyorPort CreatePort(BeltPortProfile profile, ItemConveyor.BeltT beltT, LaneSpan laneSpan)
{
// var towardStartEnd = beltT is ItemConveyor.BeltTEnd end && end.End == ItemConveyor.ConveyorEnd.End;
bool accept(IBeltItem item, float offset, LaneSpan lane)
{
if (beltT is ItemConveyor.BeltTEnd end)
{
var startBeltT = end.End == ItemConveyor.ConveyorEnd.Start ? 0 : Length;
var obstacle = GetDistanceToNextItem(ItemConveyor.SwapEnd(end.End).DirectionTo(), startBeltT, offset, lane);
var distanceAllowed = obstacle.Distance;
return ItemConveyor.ITEMSIZE < distanceAllowed;
}
else if (beltT is ItemConveyor.BeltTOffset endOffset)
{
return HasClearance(endOffset.T,lane);
// var lowerObstacle = GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardStart, endOffset.T, ItemConveyor.ITEMSIZE,LaneSpan.One);
// var upperObstacle = GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, endOffset.T, ItemConveyor.ITEMSIZE,LaneSpan.One);
// var lowerDistanceAllowed = lowerObstacle.DistanceToCenter - (lowerObstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// var upperObstacleAllowed = upperObstacle.DistanceToCenter - (upperObstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// return ItemConveyor.ITEMSIZE < lowerDistanceAllowed && ItemConveyor.ITEMSIZE < upperObstacleAllowed;
}
// return true;
throw new NotImplementedException();
}
// if (beltT is ItemConveyor.BeltTEnd end && end.End == ItemConveyor.ConveyorEnd.End){
// accept = (IBeltItem item, float offset) =>
// {
// var obstacle = GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardStart, Length, offset, LaneSpan.One);
// return ItemConveyor.ITEMSIZE < obstacle.DistanceToCenter - (obstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// };
// }
bool tryInsert(IBeltItem item, float offset, LaneSpan laneSpan)
{
if (beltT is ItemConveyor.BeltTEnd end)
{
if (end.End == ItemConveyor.ConveyorEnd.End)
{
Items.Insert(new (item, Length-offset){LaneSpan = laneSpan});
// _items.Add(new(item, Length-offset));
}
else
{
Items.Insert(new (item, offset) {LaneSpan = laneSpan});
// _items.Insert(0, new(item, offset));
}
return true;
}
else if (beltT is ItemConveyor.BeltTOffset endOffset)
{
if (!HasClearance(endOffset.T, LaneSpan.One))
{
return false;
}
Items.Insert(new (item, endOffset.T){LaneSpan = laneSpan});
// int insertIndex = FindInsertIndex(endOffset.T);
// _items.Insert(insertIndex, new(item, endOffset.T));
return true;
}
// _items.Insert(0,new ConveyorSlice(item,0));
// return true;
throw new NotImplementedException();
}
var port = new ConveyorPort(this,
profile,
beltT,
accept,
tryInsert);
return port;
}
private bool HasClearance(float centerT, LaneSpan span)
{
var lower = GetDistanceToNextItem(
ItemConveyor.BeltDirection.TowardStart,
centerT,
ItemConveyor.ITEMSIZE,
span
);
var upper = GetDistanceToNextItem(
ItemConveyor.BeltDirection.TowardEnd,
centerT,
ItemConveyor.ITEMSIZE,
span
);
float lowerAllowed =
lower.Distance;// -
// (lower.IsItem ? ItemConveyor.ITEMSIZE : 0);
float upperAllowed =
upper.Distance;// -
// (upper.IsItem ? ItemConveyor.ITEMSIZE : 0);
return ItemConveyor.ITEMSIZE < lowerAllowed &&
ItemConveyor.ITEMSIZE < upperAllowed;
}
}
// Items.Add(new ConveyorSlice(new TestItem(), 0));
// StartPort = new ConveyorPort(this,
// new BeltPortProfile(Position, Direction.Back, 1, PortAccess.BiDirectional),
// new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.Start),
// (item, offset) =>
// {
// var obstacle = GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, 0, offset, LaneSpan.One);
// return ItemConveyor.ITEMSIZE < obstacle.DistanceToCenter - (obstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// },
// (item, offset) => { _items.Insert(0, new(item, offset)); return true;});
// EndPort = new ConveyorPort(this,
// new BeltPortProfile(Position, Direction.Front, 1, PortAccess.BiDirectional),
// new ItemConveyor.BeltTEnd(ItemConveyor.ConveyorEnd.End),
// (item, offset) =>
// {
// var obstacle = GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardEnd, Length, offset, LaneSpan.One);
// return ItemConveyor.ITEMSIZE < obstacle.DistanceToCenter - (obstacle.IsItem ? ItemConveyor.ITEMSIZE : 0);
// // return ItemConveyor.ITEMSIZE < GetDistanceToNextItem(ItemConveyor.BeltDirection.TowardStart, Length, SpeedMagnitude, LaneSpan.One);
// },
// (item, offset) => { _items.Add(new(item, offset)); return true;});
//TODO Should liklely account for max search distance where the conveyorm may be needed to know
public record class BeltObstacle(float Distance)
{
// public ObstacleKind Kind { get; }
// public BeltObstacle()
// {
// }
// public float DistanceToCenter { get; } // always center / reference
// public LaneSpan LaneSpan { get; } // only meaningful for Item
// public ConveyorSlice? Item { get; } // only if Kind == Item
// public IMovementConveyor? Conveyor { get; } // boundary case
// public IBeltPort? Port { get; } // Port case
// public bool IsItem => Kind == ObstacleKind.Item;
// public bool IsBoundary => Kind == ObstacleKind.Boundary;
}
// public enum ObstacleKind
// {
// None,
// Item,
// Boundary,
// }
public record ItemBeltObstacle(float Distance, ConveyorSlice Item, LaneSpan LaneSpan) : BeltObstacle(Distance)
{
}
public record BoundaryBeltObstacle(float DistanceToBoundary) : BeltObstacle(DistanceToBoundary)
{
}
public record PortBeltObstacle(float DistanceToBoundary, LaneSpan LaneSpan, IBeltPort BeltPort) : BoundaryBeltObstacle(DistanceToBoundary)
{
}

View File

@@ -0,0 +1 @@
uid://j24fuotdwwx4

View File

@@ -0,0 +1,177 @@
namespace ChickenGameTest;
using System;
using System.Collections.Generic;
using System.Linq;
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using Godot.Collections;
using SJK.Functional;
//Will Liklely be the game instead of a node like this
[Meta(typeof(IAutoNode))]// [Tool]
public partial class VoxelGridNode : Node3D, IProvide<IVoxelGridQuery<LayeredEquipment>>, IProvide<IEquipmentComponentRegistry>, IProvide<IVoxelGridRegistry>, IProvide<IItemRenderer>
{
public override void _Notification(int what) => this.Notify(what);
private IVoxelGridQuery<LayeredEquipment> _voxelGrid = default!;
IVoxelGridQuery<LayeredEquipment> IProvide<IVoxelGridQuery<LayeredEquipment>>.Value() => _voxelGrid;
private IVoxelGridRegistry _voxelGridRegistry = default!;
IVoxelGridRegistry IProvide<IVoxelGridRegistry>.Value() => _voxelGridRegistry;
private IItemRenderer _itemRenderer = default!;
IItemRenderer IProvide<IItemRenderer>.Value() => _itemRenderer;
public override void _Ready()
{
TestStructural.Test();
GD.Print();
base._Ready();
_voxelGrid = new EquipmentVoxelGrid();
_equipmentComponentRegistry = new EquipmentComponentRegistry();
_voxelGridRegistry = new VoxelRegistry();
_itemRenderer = new TestItemRendered();
AddChild(_itemRenderer as Node);
Timer timer = new Timer() { WaitTime = .25f, Autostart = true };//TEST
AddChild(timer);//TEST
// timer.Timeout += _itemRenderer.Tick;//TEST
this.Provide();
}
public override void _Process(double delta)
{
base._Process(delta);
// GD.Print(_voxelGridRegistry.Get<ISlotFace>(Vector3I.Zero).First().GetConnectingFace());
}
IEquipmentComponentRegistry _equipmentComponentRegistry;
IEquipmentComponentRegistry IProvide<IEquipmentComponentRegistry>.Value() => _equipmentComponentRegistry;
}
// [Meta, Id("voxel_grid")]
public sealed partial class EquipmentVoxelGrid : IVoxelGridQuery<LayeredEquipment>
{
private System.Collections.Generic.Dictionary<Vector3I, LayeredEquipment> _data = new();
public void AddEquipment(Vector3I position, Equipment equipment)
{
if (!_data.TryGetValue(position, out var layers))
{
_data[position] = layers = new();
}
layers.Add(equipment);
}
//TODO Use Options instead of nu;lable
public LayeredEquipment GetVoxel(Vector3I position) => _data.TryGetValue(position, out var result) ? result : new();
public IEnumerable<Equipment> GetEquipmentAt(Vector3I position) => GetVoxel(position).GetEquipments();
// public
// public void SetEquipmentAt(Vector3I position, T value)
// {
// _data[position] = value;
// GD.Print(value);
// }
public LayeredEquipment SetVoxel(Vector3I position, LayeredEquipment value) => _data[position] = value;
public bool IsOccupied(Vector3I cell) => _data.ContainsKey(cell);
}
public interface IVoxelGridQuery<T>
{
T GetVoxel(Vector3I cell);
T SetVoxel(Vector3I cell, T value);
bool IsOccupied(Vector3I cell);
}
public class LayeredEquipment
{
public int Count => _equipment.Count;
private readonly HashSet<Equipment> _equipment = new();
public IEnumerable<Equipment> GetEquipments() => _equipment;
public bool Add(Equipment equipment) => _equipment.Add(equipment);
public bool Remove(Equipment equipment) => _equipment.Remove(equipment);
public void AddMany(params Equipment[] equipment)
{
foreach (var item in equipment)
{
Add(item);
}
}
public void RemoveMany(params Equipment[] equipment)
{
foreach (var item in equipment)
{
Remove(item);
}
}
public void IfAny(Action<IEnumerable<Equipment>> action)
{
if (Count > 0)
{
action(GetEquipments());
}
}
}
public sealed class SlotDescriptor {
public SlotDirection Direction { get; }
public SlotDescriptor(
SlotDirection direction
)
{
Direction = direction;
}
}
public abstract class SlotLogic<TPayload> {
public SlotDescriptor Descriptor { get; }
protected SlotLogic(SlotDescriptor descriptor) {
Descriptor = descriptor;
}
public abstract bool CanTransfer(
TPayload payload
);
public abstract bool TryTransfer(
TPayload payload
);
}
public sealed class ItemSlotLogic
: SlotLogic<IBeltItem> {
public ItemSlotLogic(SlotDescriptor descriptor)
: base(descriptor) {}
public override bool CanTransfer(IBeltItem item) => false;
// item.Count > 0;
public override bool TryTransfer(IBeltItem item) {
// routing rules
return true;
}
}
[Meta]
public partial class ItemSlotNode
: SlotComponentNode {
private ItemSlotLogic _logic;
public override void _Ready() {
base._Ready();
_logic = new ItemSlotLogic(
new SlotDescriptor(Direction)
);
}
public void OnResolved()
{
}
public void Tick(IBeltItem stack) {
if (_logic.CanTransfer(stack))
_logic.TryTransfer(stack);
}
}
public enum SlotDirection
{
Input,
Output
}

View File

@@ -0,0 +1 @@
uid://dcrb286hmpli

View File

@@ -0,0 +1,93 @@
[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://ee5aoxi8mjnw" path="res://src/VoxelGrid/BeltPort.cs" id="6_2wkfx"]
[ext_resource type="PackedScene" uid="uid://c4h7mwnfrdesg" path="res://src/Conveyors/ConveyorBeltStraight/ConveyorBeltStraight.tscn" id="6_mxaon"]
[sub_resource type="Curve3D" id="Curve3D_mxaon"]
_data = {
"points": PackedVector3Array(0, 0, 0, 0, 0, 0, 0, 0.5, -0.5, 0, 0, 0, 0, 0, 0, 0, 0.5, 0),
"tilts": PackedFloat32Array(0, 0)
}
point_count = 2
[node name="VoxelGridNode" type="Node3D" unique_id=825696340]
script = ExtResource("1_tsdpe")
[node name="Camera3D" type="Camera3D" parent="." unique_id=75458542]
transform = Transform3D(0.6279494, -0.32487354, 0.70720345, -8.896721e-09, 0.908705, 0.4174389, -0.77825415, -0.26213053, 0.5706208, 4.635174, 2.6038146, 2.739409)
[node name="ConveyorBeltStraight5" parent="." unique_id=975225936 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 3, 0, 0)
Width = 1
[node name="ConveyorBeltStraight22" parent="." unique_id=81137442 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 2, 0, 0)
Width = 1
[node name="ConveyorBeltStraight13" parent="." unique_id=607296315 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 0, 0, 0)
Width = 1
[node name="ConveyorBeltStraight26" parent="." unique_id=626657643 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 1, 0, 0)
Width = 1
[node name="ConveyorBeltStraight6" parent="." unique_id=264656404 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -1, 0, 0)
Width = 1
[node name="ConveyorBeltStraight23" parent="." unique_id=1438072389 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -2, 0, 0)
Width = 1
[node name="ConveyorBeltStraight14" parent="." unique_id=1045608025 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -4, 0, 0)
Width = 1
[node name="ConveyorBeltStraight27" parent="." unique_id=899289297 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -3, 0, 0)
Width = 1
[node name="ConveyorBeltStraight7" parent="." unique_id=1602707514 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 3, 0, 1)
Width = 1
[node name="ConveyorBeltStraight24" parent="." unique_id=904248251 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 2, 0, 1)
Width = 1
[node name="ConveyorBeltStraight15" parent="." unique_id=533235542 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 0, 0, 1)
Width = 1
[node name="ConveyorBeltStraight28" parent="." unique_id=2116943332 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, 1, 0, 1)
Width = 1
[node name="ConveyorBeltStraight8" parent="." unique_id=1109978762 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -1, 0, 1)
Width = 1
[node name="ConveyorBeltStraight25" parent="." unique_id=1110001269 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -2, 0, 1)
Width = 1
[node name="ConveyorBeltStraight16" parent="." unique_id=1178816500 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -4, 0, 1)
Width = 1
[node name="ConveyorBeltStraight29" parent="." unique_id=1537356869 instance=ExtResource("6_mxaon")]
transform = Transform3D(1.3113416e-07, 0, -1, 0, 1, 0, 1, 0, 1.3113416e-07, -3, 0, 1)
Width = 1
[node name="Node3D2" type="Node3D" parent="." unique_id=1815100025 node_paths=PackedStringArray("Path")]
transform = Transform3D(1.3113416e-07, 0, 1, 0, 1, 0, -1, 0, 1.3113416e-07, 4, 0, 1)
script = ExtResource("6_2wkfx")
Face = 4
Width = 1
Access = 3
Path = NodePath("Path3D")
[node name="Path3D" type="Path3D" parent="Node3D2" unique_id=1525648764]
curve = SubResource("Curve3D_mxaon")

34
test/src/GameTest.cs Normal file
View File

@@ -0,0 +1,34 @@
namespace ChickenGameTest;
using System.Threading.Tasks;
using Chickensoft.GoDotTest;
using Chickensoft.GodotTestDriver;
using Chickensoft.GodotTestDriver.Drivers;
using Godot;
using Shouldly;
public class GameTest : TestClass
{
private Game _game = default!;
private Fixture _fixture = default!;
public GameTest(Node testScene) : base(testScene) { }
[SetupAll]
public async Task Setup()
{
_fixture = new Fixture(TestScene.GetTree());
_game = await _fixture.LoadAndAddScene<Game>();
}
[CleanupAll]
public void Cleanup() => _fixture.Cleanup();
[Test]
public void TestButtonUpdatesCounter()
{
var buttonDriver = new ButtonDriver(() => _game.TestButton);
buttonDriver.ClickCenter();
_game.ButtonPresses.ShouldBe(1);
}
}

1
test/src/GameTest.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://dv3ll1ojhcavi