### Install GodotEnv Global Tool Source: https://chickensoft.games/docs/setup Use the .NET CLI to install the Chickensoft GodotEnv tool globally on your system. ```bash dotnet tool install --global Chickensoft.GodotEnv ``` -------------------------------- ### Create a reusable Godot NuGet package Source: https://chickensoft.games/docs/setup Install the GodotPackage template and initialize a new project configured for C# package development and unit testing. ```bash dotnet new --install Chickensoft.GodotPackage dotnet new chickenpackage --name "MyPackageName" --param:author "My Name" cd MyPackageName /path/to/godot4 --headless --build-solutions --quit dotnet build ``` -------------------------------- ### Install Chickensoft Godot Templates Source: https://chickensoft.games/docs/setup Commands to install project templates for Godot 4 using the .NET CLI. ```bash dotnet new install Chickensoft.GodotGame dotnet new install Chickensoft.GodotPackage ``` -------------------------------- ### Install Godot Version via GodotEnv Source: https://chickensoft.games/docs/setup Use the GodotEnv CLI to automatically download and install a specific version of Godot. ```bash godotenv godot install 4.0.1 ``` -------------------------------- ### Use Logic Blocks with Bindings Source: https://chickensoft.games/docs/logic_blocks/quick_start Start a logic block to activate its initial state. Use bindings to monitor inputs, outputs, state changes, and exceptions. Prefer monitoring outputs over state changes for more flexible code. ```csharp // Start the logic block to force the initial state to be active. // // This is optional: you can also start a logic block by just adding an // input to it or reading its state. logic.Start(); // Add an input to turn our light switch on. logic.Input(new LightSwitch.Input.Toggle()); // The logic block's value represents the current state. var state = logic.Value; // PoweredOn // Bindings allow you to observe the logic block easily. using var binding = logic.Bind(); // Monitor an output: binding.Handle((in LightSwitch.Output.StatusChanged output) => Console.WriteLine( $"Status changed to {(output.IsOn ? "on" : "off")}" ) ); // Can also use bindings to monitor inputs, state changes, and exceptions. // // In general, prefer monitoring outputs over state changes for more // flexible code. // Monitor an input: binding.Watch((in LightSwitch.Input.Toggle input) => Console.WriteLine("Toggled!") ); // Monitor a specific type of state: binding.When((LightSwitch.State.PoweredOn _) => Console.WriteLine("Powered on!") ); // Monitor all exceptions: binding.Catch((Exception e) => Console.WriteLine(e.Message)); // Monitor specific types of exceptions: binding.Catch((InvalidOperationException e) => Console.WriteLine(e.Message) ); ``` -------------------------------- ### Start and Stop Logic Block Source: https://chickensoft.games/docs/logic_blocks/basics/states Control the lifecycle of a logic block by starting and stopping it. Starting ensures the initial state is attached and entered, while stopping exits and detaches the current state. ```csharp var logic = new MyLogicBlock(); // Make sure the initial state is attached and entered. logic.Start(); // Exit and detach the current state. logic.Stop(); ``` -------------------------------- ### Interact with the Blackboard Source: https://chickensoft.games/docs/logic_blocks/basics/states Shows how to set and get dependencies from the logic block's blackboard. States can access these dependencies using the Get() method. ```csharp var logic = new MyLogicBlock(); // Add all the dependencies that states will need. logic.Set(new MyRelatedService()); var service = logic.Get(); ``` -------------------------------- ### Start LogicBlock and Observe State Entrance Source: https://chickensoft.games/docs/logic_blocks/basics/states Initiate the state machine to trigger entrance callbacks for the initial state and its parents. ```csharp logic.Start(); // Prints: // Inactive // Standing ``` -------------------------------- ### Define a LightSwitch Logic Block in C# Source: https://chickensoft.games/docs/logic_blocks This example demonstrates how to define a basic Logic Block for a light switch. It includes input handling for toggling the state and defines output for status changes. Ensure the `Chickensoft.Introspection` namespace is imported. ```csharp using Chickensoft.Introspection; [Meta, LogicBlock(typeof(State), Diagram = true)] public class LightSwitch : LogicBlock { public override Transition GetInitialState() => To(); public static class Input { public readonly record struct Toggle; } public abstract record State : StateLogic { public record PoweredOn : State, IGet { public Transition On(in Input.Toggle input) => To(); } public record PoweredOff : State, IGet { public Transition On(in Input.Toggle input) => To(); } } public static class Output { public readonly record struct StatusChanged(bool IsOn); } } ``` -------------------------------- ### Install LogicBlocks Packages Source: https://chickensoft.games/docs/logic_blocks/installation Install the LogicBlocks package, its diagram generator, and the Chickensoft Introspection generator using NuGet Package Manager. Ensure the generator packages include `PrivateAssets="all"` and `OutputItemType="analyzer"`. ```xml ``` -------------------------------- ### Recommended C# Development Settings Source: https://chickensoft.games/docs/setup Additional settings to improve the C# coding experience, including bracket guides, format-on-save behavior, and terminal profile configuration. ```json "csharp.suppressHiddenDiagnostics": false, // Draw a line between selected brackets so you can see blocks of code easier. "editor.guides.bracketPairs": "active", "[csharp]": { "editor.codeActionsOnSave": { "source.addMissingImports": "explicit", "source.fixAll": "explicit", "source.organizeImports": "explicit" }, "editor.formatOnPaste": true, "editor.formatOnSave": true, "editor.formatOnType": true }, // To make bash the default terminal on Windows, add these: "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" } } ``` -------------------------------- ### Access Blackboard Service within Attached State Source: https://chickensoft.games/docs/logic_blocks/basics/states Example of accessing a service from the logic block's blackboard within an attached state. The Get() method is available on the base StateLogic class. ```csharp [Meta, LogicBlock(typeof(State), Diagram = true)] public partial class LightSwitch : LogicBlock { public abstract record State : StateLogic { public record PoweredOn : State, IGet { public PoweredOn() { OnAttach(() => { Get().StartDoingSomething() }); } } ... } ``` -------------------------------- ### Handle StartStopButtonPressed Input Source: https://chickensoft.games/docs/logic_blocks/tutorial/power Extends the Idle state to handle the StartStopButtonPressed input, transitioning to the Countdown state. This enables starting or stopping the timer. ```csharp public static class Input { public readonly record struct PowerButtonPressed; public readonly record struct ChangeDuration(double Duration); public readonly record struct StartStopButtonPressed; } public record Idle : PoweredOn, IGet, IGet { public Transition On(in Input.ChangeDuration input) { Get().Duration = input.Duration; return ToSelf(); } public Transition On(in Input.StartStopButtonPressed input) => To(); } ``` -------------------------------- ### Mock a Logic Block in Component Tests Source: https://chickensoft.games/docs/logic_blocks/testing/testing_logic_blocks When testing a component that uses a logic block, mock the logic block's interface. This example shows how to set a specific state for the mocked timer. ```csharp using static Chickensoft.LogicBlocks.Tutorial.Timer; sealed record MyComponent(ITimer Timer) { public void DoSomething() { if (Timer.Value is State.PoweredOff) { // Do something when the timer is off. } } } public class MyComponentTest { [Fact] public void Mocks() { var timer = new Mock(); // Make the mock logic block be in a specific state. timer.Setup(t => t.Value).Returns(new State.PoweredOff()); var component = new MyComponent(timer.Object); component.DoSomething(); } } ``` -------------------------------- ### Naive Generic Logic Block (Build Error) Source: https://chickensoft.games/docs/logic_blocks/tips_and_tricks This example demonstrates a common pitfall when nesting a logic block within a generic type. Using `typeof` with a nested state type inside an attribute leads to a build error (CS0416) because the state type is implicitly generic. ```csharp // Example: nesting a logic block in a generic type public class MyGenericType { [LogicBlock(typeof(State))] // <- Error CS0416 public class MyLogicBlock : LogicBlock { public override Transition GetInitialState() => To(); public record State : StateLogic {} } } ``` -------------------------------- ### Configure .NET SDK Environment Variables Source: https://chickensoft.games/docs/setup Add these exports to your ~/.zshrc or ~/.bashrc file to set up the .NET SDK path and configuration. ```bash # .NET SDK Configuration export DOTNET_ROOT="/usr/local/share/dotnet" export DOTNET_CLI_TELEMETRY_OPTOUT=1 # Disable analytics export DOTNET_ROLL_FORWARD_TO_PRERELEASE=1 # Add the .NET SDK to the system paths so we can use the `dotnet` tool. export PATH="$DOTNET_ROOT:$PATH" export PATH="$DOTNET_ROOT/sdk:$PATH" export PATH="$HOME/.dotnet/tools:$PATH" # Run this if you ever run into errors while doing a `dotnet restore` alias nugetclean="dotnet nuget locals --clear all" ``` -------------------------------- ### Test a Logic Block's Initial State and Dependencies Source: https://chickensoft.games/docs/logic_blocks/testing/testing_logic_blocks Verify the initial state of a logic block and ensure its dependencies are correctly set up. Mock any required dependencies before creating the real logic block instance. ```csharp [Fact] public void Initializes() { // Mock dependencies that the logic block needs. var clock = new Mock(); // Create the real logic block. var timer = new Timer(); // Add the mocked dependencies to the blackboard. timer.Set(clock.Object); // Check that the initial state is the one we expect. var state = timer.GetInitialState() .State .ShouldBeOfType(); // Verify the timer has set its blackboard data correctly. timer.Has().ShouldBeTrue(); timer.Get().ShouldBe(clock.Object); } ``` -------------------------------- ### Instantiate and Use DimmableLightSwitch Input Source: https://chickensoft.games/docs/logic_blocks/basics/inputs Instantiate your logic block and then call the 'Input' method, passing a new instance of the input struct with the relevant data. This pattern is used to trigger actions within the logic block. ```csharp var dimmableLightSwitch = new DimmableLightSwitch(); dimmdableLightSwitch.Input(new DimmableLightSwitch.Input.DimmerUpdated(0.5d)); ``` -------------------------------- ### Test State with Dependency Injection Source: https://chickensoft.games/docs/logic_blocks/testing/testing_states Add dependencies to the fake context's blackboard using `context.Set()` to allow states to use them during testing. Retrieve dependencies with `context.Get()`. ```csharp [Fact] public void ChangesDuration() { var state = new Timer.State.PoweredOn.Idle(); var context = state.CreateFakeContext(); // Put a value on the blackboard for the state to use. context.Set(new Timer.Data() { Duration = 30.0d }); var duration = 45; state.On(new Timer.Input.ChangeDuration(duration)) .State .ShouldBeOfType(); context.Get().Duration.ShouldBe(duration); } ``` -------------------------------- ### Handle an Output Source: https://chickensoft.games/docs/logic_blocks/basics/outputs Use a binding to monitor and respond to produced outputs. ```csharp using var binding = lightSwitch.Bind(); // Monitor an output: binding.Handle((in LightSwitch.Output.StatusChanged output) => System.Console.WriteLine( $"Status changed to {(output.IsOn ? "on" : "off")}" ) ); ``` -------------------------------- ### Handle Multiple Inputs in a State Source: https://chickensoft.games/docs/logic_blocks/basics/states Demonstrates how a state can handle multiple input types (A, B, C) by implementing IGet. Use ToSelf() to remain in the current state or To() to transition to a new state. ```csharp public record MyState : State, IGet, IGet, IGet { // Don't change states on A public Transition On(in Input.A input) => ToSelf(); // Go to StateC on C public Transition On(in Input.C input) => To(); } ``` -------------------------------- ### Create a new Godot game project Source: https://chickensoft.games/docs/setup Use the chickengame template to initialize a new Godot 4 project with pre-configured debug, testing, and CI/CD settings. ```bash dotnet new chickengame --name "MyGameName" --param:author "My Name" cd MyGameName dotnet restore ``` -------------------------------- ### Observe Outputs with Bindings Source: https://chickensoft.games/docs/logic_blocks/basics/bindings Use the Handle method to process specific outputs emitted by a logic block. ```csharp using var binding = logic.Bind(); binding.Handle((in MyLogicBlock.Output.SomeOutput output) => { // Handle a particular output. }); ``` -------------------------------- ### Essential C# Extension Settings Source: https://chickensoft.games/docs/setup Basic configuration to ensure project compatibility with the C# extension in VSCode. ```json "dotnetAcquisitionExtension.enableTelemetry": false, // Increases project compatibility with the C# extension. "dotnet.preferCSharpExtension": true, ``` -------------------------------- ### Test State Outputs with Fake Context Source: https://chickensoft.games/docs/logic_blocks/testing/testing_states Use `CreateFakeContext()` to test state outputs. Simulate state entry with `Enter()` and verify expected outputs using `context.Outputs`. ```csharp [Fact] public void PlaysBeepingSoundOnEnter() { var state = new Timer.State.PoweredOn.Beeping(); // Create a fake context for testing purposes. var context = state.CreateFakeContext(); // Simulate the state being entered. state.Enter(); // Verify that the state produced the outputs we expect. context.Outputs.ShouldBe([new Timer.Output.PlayBeepingSound()]); } ``` -------------------------------- ### Create a Logic Block with Chickensoft Source: https://chickensoft.games/docs/logic_blocks/quick_start Define a logic block by extending LogicBlock, overriding GetInitialState(), and adding the [LogicBlock] attribute. Use nested static classes for inputs and outputs, and nested records for states. ```csharp using Chickensoft.Introspection; [Meta, LogicBlock(typeof(State), Diagram = true)] public partial class LightSwitch : LogicBlock { // Define your initial state here. public override Transition GetInitialState() => To(); // By convention, inputs are defined in a static nested class called Input. public static class Input { public readonly record struct Toggle; } // By convention, outputs are defined in a static nested class called Output. public static class Output { public readonly record struct StatusChanged(bool IsOn); } // To reduce unnecessary heap allocations, inputs and outputs should be // readonly record structs. // By convention, the base state type is nested inside the logic block. This // helps the logic block diagram generator know where to search for state // types. public abstract record State : StateLogic { // Substates are sometimes nested inside their parent states to help // organize the code. // On state. public record PoweredOn : State, IGet { public PoweredOn() { // Announce that we are now on. this.OnEnter(() => Output(new Output.StatusChanged(IsOn: true))); } public Transition On(in Input.Toggle input) => To(); } // Off state. public record PoweredOff : State, IGet { public PoweredOff() { // Announce that we are now off. this.OnEnter(() => Output(new Output.StatusChanged(IsOn: false))); } public Transition On(in Input.Toggle input) => To(); } } } ``` -------------------------------- ### Configure project features for C# Source: https://chickensoft.games/docs/how_csharp_works The project.godot file is updated to include the C# feature flag when a C# script is added to a node. ```ini [application] config/features=PackedStringArray("4.4", "C#", "Mobile") ``` -------------------------------- ### Add NuGet Package Reference Source: https://chickensoft.games/docs/how_csharp_works Add a standard NuGet package to your .csproj file using the ItemGroup element. ```xml ``` -------------------------------- ### Initialize Fake Binding Source: https://chickensoft.games/docs/logic_blocks/testing/testing_bindings Use the static CreateFakeBinding method to generate a binding for a mocked logic block. ```csharp var logic = new Mock(); // CreateFakeBinding() is actually a static method on the logic block. var binding = MyLogicBlock.CreateFakeBinding(); // Make our mock logic block return the fake binding. logic.Setup(logic => logic.Bind()).Returns(binding); ``` -------------------------------- ### Observe Inputs with Bindings Source: https://chickensoft.games/docs/logic_blocks/basics/bindings Use the Watch method to listen for specific inputs sent to a logic block. ```csharp using var binding = logic.Bind(); binding.Watch((in MyLogicBlock.Input.SomeInput input) => { // Watch for a particular input. }); ``` -------------------------------- ### Enable compiler generated file output Source: https://chickensoft.games/docs/how_csharp_works Add these properties to your .csproj file to inspect the code generated by Godot's C# source generators. ```xml true .generated ``` -------------------------------- ### Define global.json for SDK versioning Source: https://chickensoft.games/docs/how_csharp_works Use a global.json file to manage SDK versions centrally, allowing for easier automated updates. ```json { "msbuild-sdks": { "Godot.NET.Sdk": "4.4.0" }, "sdk": { "rollForward": "major", "version": "8.0.401" } } ``` -------------------------------- ### Manually Invoke State Entrance and Exit Callbacks Source: https://chickensoft.games/docs/logic_blocks/testing/testing_states Manually invoke entrance and exit callbacks for a state by calling `Enter()` and `Exit()` on the state instance. A fake context is required. ```csharp var context = state.CreateFakeContext(); // Simulate the state being entered. state.Enter(); // Simulate the state being exited. state.Exit(); ``` -------------------------------- ### Reference Godot.NET.Sdk Source: https://chickensoft.games/docs/how_csharp_works The project file references the Godot.NET.Sdk to include necessary MSBuild targets and source generators. ```xml ``` ```xml ``` -------------------------------- ### Compare Direct Manipulation and Outputs Source: https://chickensoft.games/docs/logic_blocks/basics/outputs Directly calling service methods versus producing outputs for external listeners. ```csharp public Transition On(in Input.SomethingHappened input) { Get().ChangeSomething(input.Value); return ToSelf(); } ``` ```csharp public Transition On(in Input.SomethingHappened input) { Output(new Output.ChangeSomething(input.Value)); return ToSelf(); } ``` -------------------------------- ### Define State Entrance and Exit Callbacks Source: https://chickensoft.games/docs/logic_blocks/basics/states Define OnEnter and OnExit callbacks for a state. These methods must be invoked using 'this.OnEnter' and 'this.OnExit' as they are extension methods. ```csharp public MyState() { this.OnEnter(() => System.Console.WriteLine("MyState entered.")) this.OnExit(() => System.Console.WriteLine("MyState exited.")) } ``` -------------------------------- ### Simulate LogicBlock Interactions Source: https://chickensoft.games/docs/logic_blocks/testing/testing_bindings Use fake bindings to trigger inputs, outputs, errors, and state changes in tests. ```csharp var logic = new Mock(); var binding = MyLogicBlock.CreateFakeBinding(); logic.Setup(logic => logic.Bind()).Returns(binding); // Simulate an input with our fake binding. binding.Input(new MyLogicBlock.Input.SomeInput()); ``` ```csharp var logic = new Mock(); var binding = MyLogicBlock.CreateFakeBinding(); logic.Setup(logic => logic.Bind()).Returns(binding); // Simulate an input with our fake binding. binding.Output(new MyLogicBlock.Output.SomeOutput()); ``` ```csharp var logic = new Mock(); var binding = MyLogicBlock.CreateFakeBinding(); logic.Setup(logic => logic.Bind()).Returns(binding); // Simulate an error with our fake binding. binding.AddError(new InvalidOperationException()); ``` ```csharp var logic = new Mock(); var binding = MyLogicBlock.CreateFakeBinding(); logic.Setup(logic => logic.Bind()).Returns(binding); // Simulate a state change with our fake binding. binding.SetState(new MyLogicBlock.State.SomeOtherState()); ``` -------------------------------- ### Define a LogicBlock class Source: https://chickensoft.games/docs/logic_blocks/tutorial/timer_logic_block Create a partial class extending LogicBlock with the Meta and LogicBlock attributes for introspection support. ```csharp using Chickensoft.Introspection; [Meta, LogicBlock(typeof(State), Diagram = true)] public partial class Timer : LogicBlock { public override Transition GetInitialState() => To(); public static class Input; public static class Output; public abstract partial record State : StateLogic { public record PoweredOff : State; } } ``` -------------------------------- ### Create Manual Logic Block Source: https://chickensoft.games/docs/logic_blocks/tips_and_tricks Implement a logic block without the Introspection generator by omitting the [Meta] attribute and manually preallocating states in the constructor. Ensure all possible states are added to the blackboard to prevent runtime errors. ```csharp [LogicBlock(typeof(State))] public class ManualLogicBlock : LogicBlock { public abstract record State : StateLogic { } public record StateOne : State; public record StateTwo : State; public override Transition GetInitialState() => To(); public ManualLogicBlock() { // Important: we have to add an instance of each state to the blackboard // to avoid errors at runtime. Set(new StateOne()); Set(new StateTwo()); } } ``` -------------------------------- ### Test State Inputs and Errors Source: https://chickensoft.games/docs/logic_blocks/testing/testing_states Verify state inputs and errors by accessing `context.Inputs` and `context.Errors` respectively after simulating state interactions. ```csharp // You can also verify inputs and errors in the same way via context.Inputs and context.Errors. ``` -------------------------------- ### Transition Between Compound States Source: https://chickensoft.games/docs/logic_blocks/basics/states Transitioning between substates of the same parent triggers only the callbacks for the changed portion of the hierarchy. ```text Active Walking ``` ```text Running ``` -------------------------------- ### Timer Data Structure and Initialization Source: https://chickensoft.games/docs/logic_blocks/tutorial/power Defines a `Data` record to hold shared timer values (Duration, TimeRemaining) and initializes it on the blackboard. This is used for sharing data between states. ```csharp public sealed record Data { public double Duration { get; set; } public double TimeRemaining { get; set; } } public Timer() { // Set shared data for all states in the blackboard. Set(new Data() { Duration = 30.0d }); } ``` -------------------------------- ### Produce an Output Source: https://chickensoft.games/docs/logic_blocks/basics/outputs Call the Output method from within a state to emit an event. ```csharp public record PoweredOn : State { public PoweredOn() { this.OnEnter(() => // Produce an output when we enter this state. Output(new Output.StatusChanged(IsOn: true)) ); } } ``` -------------------------------- ### Define Timer States and Inputs Source: https://chickensoft.games/docs/logic_blocks/tutorial/power Defines the Timer logic block with initial states (PoweredOff, PoweredOn.Idle) and input events (PowerButtonPressed). Use this to set up the basic state machine for the timer. ```csharp [Meta, LogicBlock(typeof(State), Diagram = true)] public partial class Timer : LogicBlock { public override Transition GetInitialState() => To(); public static class Input { public readonly record struct PowerButtonPressed; } public static class Output; public abstract record State : StateLogic { public record PoweredOff : State, IGet { public Transition On(in Input.PowerButtonPressed input) => To(); } public abstract record PoweredOn : State, IGet { public Transition On(in Input.PowerButtonPressed input) => To(); public record Idle : PoweredOn; } } } ``` -------------------------------- ### Observe State Changes with Bindings Source: https://chickensoft.games/docs/logic_blocks/basics/bindings Use the When method to trigger logic when the state transitions to a specific type. ```csharp using var binding = logic.Bind(); binding.When(state => { // Respond to a state change. This is only called when changing from a state // that is not the type specified. }); ``` -------------------------------- ### Define an Output Source: https://chickensoft.games/docs/logic_blocks/basics/outputs Outputs are typically stored in a static class named Output within the logic block. ```csharp partial class DimmableLightSwitch { public static class Output { public readonly record struct StatusChanged(bool IsOn); } } ``` -------------------------------- ### Configure GODOT environment variable for macOS Source: https://chickensoft.games/docs/setup Add this export statement to your ~/.zshrc file to point to the Godot binary location. ```bash # This should be added to your ~/.zshrc file by GodotEnv automatically, but # you can also add it manually and change the path of Godot to match # your system. export GODOT="/Users/{you}/.config/godotenv/godot/bin" ``` -------------------------------- ### Observe Errors with Bindings Source: https://chickensoft.games/docs/logic_blocks/basics/bindings Use the Catch method to handle exceptions thrown within the logic block. ```csharp using var binding = logic.Bind(); binding.Catch(e => { // Catch an error. }); ``` -------------------------------- ### Define DimmableLightSwitch Input Structure Source: https://chickensoft.games/docs/logic_blocks/basics/inputs Define a static class named 'Input' within your logic block to hold input types. Use record structs for inputs to leverage C# 10 primary constructor syntax and ensure immutability with the 'readonly' modifier. ```csharp partial class DimmableLightSwitch { public static class Input { public readonly record struct DimmerUpdated(double Value); } } ``` -------------------------------- ### Access Blackboard Objects via Interfaces Source: https://chickensoft.games/docs/logic_blocks/basics/outputs Restrict logic block interactions by storing and retrieving objects using interface types. ```csharp logic = new MyLogicBlock(); logic.Set(new MyRelatedService() as IMyRelatedService) // or, same as above: logic.Set(new MyRelatedService()) // elsewhere, in your logic block state public Transition On(in Input.SomethingHappened input) { // must access by the type it was stored as var something = Get().GrabSomethingForMe(); // ... return ToSelf(); } ``` -------------------------------- ### Create Test Implementation for Abstract State Source: https://chickensoft.games/docs/logic_blocks/serialization When testing abstract logic block states, create a concrete test implementation. Ensure this test state is introspective and identifiable by adding `[Meta, Id]` attributes to prevent errors during logic block creation. ```csharp public class MyTest { public class SomeState : SerializableLogicBlock.SomeAbstractState; [Fact] public void Initializes() => new TestState().ShouldNotBeNull(); } ``` ```csharp [Meta, Id("serializable_logic_test_some_state")] public partial class SomeState : SerializableLogicBlock.SomeAbstractState; ``` ```csharp [TestState, Meta, Id("serializable_logic_test_some_state")] public partial class SomeState : SerializableLogicBlock.SomeAbstractState; ``` -------------------------------- ### Define LogicBlock Interface Source: https://chickensoft.games/docs/logic_blocks/testing/testing_bindings Create an interface for the logic block to enable mocking during tests. ```csharp public interface IMyLogicBlock : ILogicBlock { } [Meta, LogicBlock(typeof(State))] public partial class MyLogicBlock : LogicBlock, IMyLogicBlock { public record State : StateLogic { ... } } ``` -------------------------------- ### Define Beeping State and Outputs Source: https://chickensoft.games/docs/logic_blocks/tutorial/beeping Defines the output records for sound control and the Beeping state class with entry/exit callbacks and input handling. ```csharp public static class Output { public readonly record struct PlayBeepingSound; public readonly record struct StopBeepingSound; } public record Beeping : PoweredOn, IGet { public Beeping() { this.OnEnter(() => Output(new Output.PlayBeepingSound())); this.OnExit(() => Output(new Output.StopBeepingSound())); } public Transition On(in Input.StartStopButtonPressed input) => To(); } ``` -------------------------------- ### Serialize a Logic Block to JSON Source: https://chickensoft.games/docs/logic_blocks/serialization Use JsonSerializerOptions with the SerializableTypeResolver and SerializableTypeConverter from Chickensoft.Serialization to serialize a logic block. The [Id] attribute serves as the type discriminator for stable JSON output. ```csharp var options = new JsonSerializerOptions { WriteIndented = true, // Use the type resolver and converter from the // Chickensoft.Serialization package. You can combine these with other // type resolvers and converters. TypeInfoResolver = new SerializableTypeResolver(), Converters = { new SerializableTypeConverter() } }; var logic = new SerializableLogicBlock(); var json = JsonSerializer.Serialize(logic, options); /* Produces """ { "$type": "serializable_logic", "$v": 1, "state": { "$type": "serializable_logic_state_off", "$v": 1 }, "blackboard": { "$type": "blackboard", "$v": 1, "values": {} } } """ */ ``` -------------------------------- ### Triggering State Changes via Inputs in C# Source: https://chickensoft.games/docs/logic_blocks/basics/states Use the Input method within event handlers to force state transitions reactively. Avoid adding inputs directly within lifecycle callbacks to prevent unexpected behavior. ```csharp public record MyState : State, IGet { public MyState() { OnAttach(() => Get().DataReceived += OnDataReceived) OnDetach(() => Get().DataReceived -= OnDataReceived) } private void OnDataReceived(int data) { // Trigger an input on the logic block that owns us — this can force a // state change reactively if we know we can handle this type of input. Input(new Input.SomethingHappened(data)); } public Transition On(in Input.SomethingHappened input) => To(); } ``` -------------------------------- ### Test State Attachment and Detachment Source: https://chickensoft.games/docs/logic_blocks/testing/testing_states Simulate a state being attached to or detached from a logic block by calling `Attach()` and `Detach()` respectively, passing the relevant context. ```csharp var state = new Timer.State.PoweredOn.Idle(); var context = state.CreateFakeContext(); // Simulate the state being attached to a logic block. state.Attach(context); // Simulate the state being detached from a logic block. state.Detach(context); ``` -------------------------------- ### Define an Interface for a Mockable Logic Block Source: https://chickensoft.games/docs/logic_blocks/testing/testing_logic_blocks To make a logic block easily mockable, implement an interface. This allows you to mock its API during tests. ```csharp public interface ITimer : ILogicBlock; [Meta, LogicBlock(typeof(State), Diagram = true)] public partial class Timer : LogicBlock, ITimer { public override Transition GetInitialState() => To(); ... } ``` -------------------------------- ### Add Source Generator Package Source: https://chickensoft.games/docs/how_csharp_works Include source generator packages by specifying PrivateAssets and OutputItemType attributes. ```xml ``` -------------------------------- ### Define Compound States with Inheritance Source: https://chickensoft.games/docs/logic_blocks/basics/states Create hierarchical states by inheriting from base state records. The constructor execution order determines the state hierarchy. ```csharp [Meta, LogicBlock(typeof(State))] public partial class ExerciseLogic : LogicBlock { public override Transition GetInitialState() => To(); public abstract record State : StateLogic; public abstract record Active : State { public Active() { this.OnEnter(() => Console.WriteLine("Active")); } public record Walking : Active { public Walking() { this.OnEnter(() => Console.WriteLine("Walking")); } } public record Running : Active { public Running() { this.OnEnter(() => Console.WriteLine("Running")); } } } public abstract record Inactive : State { public Inactive() { this.OnEnter(() => Console.WriteLine("Inactive")); } public record Standing : Inactive { public Standing() { this.OnEnter(() => Console.WriteLine("Standing")); } } } } ``` -------------------------------- ### Configure WarningsAsErrors Source: https://chickensoft.games/docs/logic_blocks/installation Add the `WarningsAsErrors` element to your .csproj file's `PropertyGroup` to treat warning CS9057 as an error. This helps catch potential compiler-mismatch issues with the Introspection generator. ```xml net8.0 ... CS9057 ... ``` -------------------------------- ### C# Semantic Syntax Highlighting Color Correction Source: https://chickensoft.games/docs/setup Custom color rules for documentation comments to improve readability across different theme types. ```json "editor.tokenColorCustomizations": { "[*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" } } ] }, "[*]": { // 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" } } ] } }, ``` -------------------------------- ### Define a LightSwitch Logic Block with States Source: https://chickensoft.games/docs/logic_blocks/basics/states Defines a logic block for a light switch with 'PoweredOn' and 'PoweredOff' states. States are record classes derived from StateLogic. Use Meta and LogicBlock attributes for introspection and diagram generation. ```csharp using Chickensoft.Introspection; [Meta, LogicBlock(typeof(State), Diagram = true)] public partial class LightSwitch : LogicBlock { // Define the state. By convention, this is placed inside the logic block. public abstract record State : StateLogic { // On state. public record PoweredOn : State, IGet {} // Off state. public record PoweredOff : State, IGet { public Transition On(in Input.Toggle input) => To(); } } // Define your initial state here. public override Transition GetInitialState() => To(); } ``` -------------------------------- ### Save Blackboard Values for Serialization Source: https://chickensoft.games/docs/logic_blocks/serialization Register blackboard values to be persisted when serializing a logic block. Ensure types are introspective and identifiable. Values are created via a factory closure for lazy instantiation. ```csharp [Meta, Id("my_related_data")] public partial record MyRelatedData { public required string Name { get; init; } public string? Description { get; init; } } var logic = new SerializableLogicBlock(); // Not persisted — just adding a runtime dependency. logic.Set(new MyRelatedService()); // Will be persisted if we serialize the logic block. // Types saved this way must be introspective, identifiable types, too. logic.Save(() => new MyRelatedData()); ``` -------------------------------- ### Manually register states in constructor Source: https://chickensoft.games/docs/logic_blocks/tutorial/timer_logic_block Register each state manually in the constructor if the introspection generator is not used. ```csharp public Timer() { Set(new PoweredOff()); // Do this for each possible state. } ``` -------------------------------- ### Invoke Entrance and Exit Callbacks for Subsections Source: https://chickensoft.games/docs/logic_blocks/testing/testing_states To invoke entrance and exit callbacks for a specific subsection of a state's type hierarchy, pass the parent state type to `Enter()` or `Exit()`. This ignores callbacks from the parent and its ancestors. ```csharp var state = new Timer.State.PoweredOn.Idle(); // If PoweredOn had entrance callbacks, they wouldn't be run. This only runs // the entrance callbacks for Idle, if any. state.Enter(); // Same as above, but for exiting. state.Exit(); ``` -------------------------------- ### Implement Countdown State Source: https://chickensoft.games/docs/logic_blocks/tutorial/countdown Implements the Countdown state for the timer. It subscribes to the IClock.TimeElapsed event on attach and unsubscribes on detach. Handles TimeElapsed input to decrement time remaining and StartStopButtonPressed to transition to Idle. ```csharp public record Countdown : PoweredOn, IGet, IGet { public Countdown() { OnAttach(() => Get().TimeElapsed += OnTimeElapsed); OnDetach(() => Get().TimeElapsed -= OnTimeElapsed); } private void OnTimeElapsed(double delta) => Input(new Input.TimeElapsed(delta)); public Transition On(in Input.TimeElapsed input) { var data = Get(); data.TimeRemaining -= input.Delta; return data.TimeRemaining <= 0.0d ? To() : ToSelf(); } public Transition On(in Input.StartStopButtonPressed input) => To(); } ``` -------------------------------- ### Restore Logic Block State from Deserialized Object Source: https://chickensoft.games/docs/logic_blocks/serialization Copy the state and blackboard values from a deserialized logic block into an existing logic block using the RestoreFrom method. This preserves existing bindings while updating the logic block's state. ```csharp var logic = JsonSerializer.Deserialize( json, options ); // Copy the state and blackboard of the deserialized logic block into an // existing logic block. existingLogicBlock.RestoreFrom(logic); // Now our existing logic block is in the same state and has the same blackboard // values as the logic block we deserialized, allowing us to continue where we // left off. ``` -------------------------------- ### Define Versioned Logic Block States Source: https://chickensoft.games/docs/logic_blocks/serialization Implement versioning for logic block states by applying the `[Version]` attribute to derived types. The `[Id]` attribute should be on an abstract base type. Type hierarchies can be rearranged as long as type identity and shape remain stable. ```csharp [Meta, Id("serializable_logic_versioned_state")] public abstract partial record VersionedState; [Meta, Version(1)] public partial record Version1 : VersionedState; [Meta, Version(2)] public partial record Version2 : VersionedState; ``` -------------------------------- ### Define Timer Input Types Source: https://chickensoft.games/docs/logic_blocks/tutorial/countdown Defines the input record structs for the timer, including TimeElapsed and StartStopButtonPressed. These are used to communicate events to the timer states. ```csharp public static class Input { public readonly record struct PowerButtonPressed; public readonly record struct ChangeDuration(double Duration); public readonly record struct StartStopButtonPressed; public readonly record struct TimeElapsed(double Delta); } ``` -------------------------------- ### Deserialize a Logic Block from JSON Source: https://chickensoft.games/docs/logic_blocks/serialization Deserialize a JSON string back into a SerializableLogicBlock instance using the same JsonSerializerOptions configured with Chickensoft's serialization components. ```csharp // using the same serialization options shown above var logic = JsonSerializer.Deserialize( json, options ); ```