From a6ae9785a9469306cada73b30305543d12ddc2ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 19:11:03 +0000 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20chore:=20update?= =?UTF-8?q?=20generated=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/manifest.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/manifest.json b/data/manifest.json index 3b41b09d4..f59cfe2e9 100644 --- a/data/manifest.json +++ b/data/manifest.json @@ -1013,7 +1013,7 @@ }, { "id": "#bs.hitbox:intangible", - "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/hitbox.html#entities", + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/hitbox.html#blocks", "authors": [ "Aksiome" ], @@ -1022,13 +1022,13 @@ "minecraft_version": "1.21" }, "updated": { - "date": "2024/09/28", - "minecraft_version": "1.21" + "date": "2025/03/10", + "minecraft_version": "1.21.4" } }, { "id": "#bs.hitbox:intangible", - "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/hitbox.html#blocks", + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/hitbox.html#entities", "authors": [ "Aksiome" ], @@ -1037,8 +1037,8 @@ "minecraft_version": "1.21" }, "updated": { - "date": "2025/03/10", - "minecraft_version": "1.21.4" + "date": "2024/09/28", + "minecraft_version": "1.21" } }, { From da9b26072b207c8f610f002829b488f7ac1055df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Wed, 16 Jul 2025 18:35:38 +0200 Subject: [PATCH 02/14] Progress on FSM --- .../data/bs.fsm/function/__load__.mcfunction | 1 + .../data/bs.fsm/function/cancel.mcfunction | 3 ++ .../function/check/acceptability.mcfunction | 10 +++++ .../function/check/initiality.mcfunction | 3 ++ .../check/internal/explore_state.mcfunction | 7 ++++ .../internal/get_input_states.mcfunction | 6 +++ .../check/internal/initiality.mcfunction | 10 +++++ .../function/check/reachability.mcfunction | 16 ++++++++ .../data/bs.fsm/function/delete.mcfunction | 2 + .../data/bs.fsm/function/new.mcfunction | 40 +++++++++++++++++++ .../data/bs.fsm/function/start.mcfunction | 4 ++ modules/bs.fsm/module.json | 17 ++++++++ 12 files changed, 119 insertions(+) create mode 100644 modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/cancel.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/delete.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/new.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/start.mcfunction create mode 100644 modules/bs.fsm/module.json diff --git a/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction new file mode 100644 index 000000000..a29f9b538 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction @@ -0,0 +1 @@ +execute unless data storage bs:data fsm run data modify storage bs:data fsm set value { fsm: {}, running_instances: {} } diff --git a/modules/bs.fsm/data/bs.fsm/function/cancel.mcfunction b/modules/bs.fsm/data/bs.fsm/function/cancel.mcfunction new file mode 100644 index 000000000..da4114d75 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/cancel.mcfunction @@ -0,0 +1,3 @@ +# Input: +# Macro: instance_name: string +# Macro: bind: "global" | "local" diff --git a/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction new file mode 100644 index 000000000..f793651da --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction @@ -0,0 +1,10 @@ +# Input: +# Storage: bs:ctx _.fsm (a FSM) + +# Output: +# Storage: bs:ctx _.finals (a list of states) +# Return 0 or 1 (0 if the FSM is not acceptable, 1 if it is) + +data modify storage bs:ctx _.finals set value [] +data modify storage bs:ctx _.finals append from storage bs:ctx _.fsm.states[{final: true}] +return run execute if data storage bs:ctx _.finals[0] diff --git a/modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction new file mode 100644 index 000000000..95c68cf6d --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction @@ -0,0 +1,3 @@ +# Input: +# Storage: bs:ctx _.fsm (a FSM) +return run function bs.fsm:check/internal/initiality with storage bs:ctx _.fsm diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction new file mode 100644 index 000000000..8354e4ce1 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction @@ -0,0 +1,7 @@ +# Input: +# Storage: bs:ctx _.states_to_browse (a list of states name) +# Storage: bs:ctx _.states_to_find (a list of states name) +# Storage: bs:ctx _.fsm (a FSM) + +data modify storage bs:ctx _.current_state set from storage bs:ctx _.states_to_browse[-1] +function bs.fsm:check/internal/get_input_states with storage bs:ctx _ diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction new file mode 100644 index 000000000..fc56fec62 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction @@ -0,0 +1,6 @@ +# Input: +# Macro: $(current_state) (a state name) +# Storage: bs:ctx _.fsm (a FSM) + +data modify storage bs:ctx _.source_states set value [] +$data modify storage bs:ctx _.source_states append from storage bs:ctx _.fsm.states[{transitions: [{to: $(current_state)}]}] diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction new file mode 100644 index 000000000..44f252efe --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction @@ -0,0 +1,10 @@ +# Input: +# Storage: bs:ctx _.fsm (a FSM) +# Macro: initial: state + +# Output: +# Storage: bs:ctx _.initial (a state) +# Return 0 or 1 (0 if the state does not exist, 1 if it does) + +data remove storage bs:ctx _.initial +$return run data modify storage bs:ctx _.initial set from storage bs:ctx _.fsm.states[{name: $(initial)}] diff --git a/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction new file mode 100644 index 000000000..e30200caf --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction @@ -0,0 +1,16 @@ +# Input: +# Storage: bs:ctx _.fsm (a FSM) +# Storage: bs:ctx _.finals (a list of states) +# Storage: bs:ctx _.initial (a state) + +# Storage: bs:ctx _.found_states (a list of states name) + +data modify storage bs:ctx _.non_final_states set value [] +data modify storage bs:ctx _.non_final_states append from storage bs:ctx _.fsm.states[] +data remove storage bs:ctx _.non_final_states[{final: true}] + +data modify storage bs:ctx _.states_to_find set value [] +data modify storage bs:ctx _.states_to_find append from storage bs:ctx _.non_final_states[].name + +data modify storage bs:ctx _.states_to_browse set value [] +data modify storage bs:ctx _.states_to_browse append from storage bs:ctx _.finals[].name diff --git a/modules/bs.fsm/data/bs.fsm/function/delete.mcfunction b/modules/bs.fsm/data/bs.fsm/function/delete.mcfunction new file mode 100644 index 000000000..161a9e269 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/delete.mcfunction @@ -0,0 +1,2 @@ +# Input: +# Macro: fsm_name: string diff --git a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction new file mode 100644 index 000000000..d5a5f1553 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction @@ -0,0 +1,40 @@ +# Input: +# Macro: name: string +# Macro: fsm: { +# initial: state +# on_cancel: function +# states: [ +# { +# name: string +# on_tick: function +# on_exit: function +# on_enter: function +# final?: boolean +# transitions?: [ +# { +# name?: string +# condition: 'manual' | { type: 'predicate', wait: string } | { type: 'function', wait: string } | { type: 'hook', wait: string } | { type: 'delay', wait: string } +# to: state +# } +# ] +# } +# ] +# } + + +# Check if the FSM already exists. +$execute if data storage bs:data fsm.fsm.'$(name)' run function #bs.log:error { \ + namespace: bs.fsm, \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: ["A FSM with the name '$(name)' already exists."] \ +} +$execute if data storage bs:data fsm.fsm.'$(name)' run return fail + +# Check if the initial state exists. +$data modify storage bs:ctx _ set value { fsm: $(fsm) } + + + + +$data modify storage bs:data fsm.fsm.'$(name)' set value $(fsm) diff --git a/modules/bs.fsm/data/bs.fsm/function/start.mcfunction b/modules/bs.fsm/data/bs.fsm/function/start.mcfunction new file mode 100644 index 000000000..f4932fb33 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/start.mcfunction @@ -0,0 +1,4 @@ +# Input: +# Macro: fsm_name: string +# Macro: instance_name: string +# Macro: bind: "global" | "local" diff --git a/modules/bs.fsm/module.json b/modules/bs.fsm/module.json new file mode 100644 index 000000000..95683eedb --- /dev/null +++ b/modules/bs.fsm/module.json @@ -0,0 +1,17 @@ +{ + "extend": "../config.json", + "data_pack": { + "name": "bs.fsm", + "load": "." + }, + "meta": { + "name": "Final State Machine", + "slug": "bookshelf-fsm", + "description": "Bookshelf final state machine module.", + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html", + "tags": ["runtime"], + "dependencies": [ + "bs.random" + ] + } +} From 86c4ad6c3bb9931f2e688992c9b68b67d8fc0d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Wed, 16 Jul 2025 21:35:04 +0200 Subject: [PATCH 03/14] Add various checks --- .../data/bs.fsm/function/__load__.mcfunction | 3 ++ .../function/check/acceptability.mcfunction | 23 ++++++++- .../function/check/initiality.mcfunction | 13 ++++- .../check/internal/explore_state.mcfunction | 16 +++++++ .../internal/get_input_states.mcfunction | 3 ++ .../check/internal/select_state.mcfunction | 9 ++++ .../internal/update_found_states.mcfunction | 34 ++++++++++++++ .../bs.fsm/function/check/is_valid.mcfunction | 12 +++++ .../function/check/reachability.mcfunction | 47 +++++++++++++++++-- .../bs.fsm/function/check/unicity.mcfunction | 34 ++++++++++++++ .../data/bs.fsm/function/new.mcfunction | 7 +-- 11 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction diff --git a/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction index a29f9b538..f5419eb51 100644 --- a/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction @@ -1 +1,4 @@ +forceload add -30000000 1600 + +execute unless entity B5-0-0-0-1 run summon minecraft:marker -30000000 0 1600 {UUID:[I;181,0,0,1],Tags:["bs.entity","bs.persistent","smithed.entity","smithed.strict"]} execute unless data storage bs:data fsm run data modify storage bs:data fsm set value { fsm: {}, running_instances: {} } diff --git a/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction index f793651da..02fc5c176 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction @@ -3,8 +3,27 @@ # Output: # Storage: bs:ctx _.finals (a list of states) -# Return 0 or 1 (0 if the FSM is not acceptable, 1 if it is) +# Return 0 or 1 (0/fail if the FSM is not acceptable, 1 if it is) + +# Goal: check if the FSM is acceptable, ie, if it has at least one final state +# Also check if the final states do not have any transition data modify storage bs:ctx _.finals set value [] data modify storage bs:ctx _.finals append from storage bs:ctx _.fsm.states[{final: true}] -return run execute if data storage bs:ctx _.finals[0] +execute if data storage bs:ctx _.finals[0] run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "The FSM has no final state."}] \ +} +execute if data storage bs:ctx _.finals[0] run return fail + +execute if data storage bs:ctx _.finals[].transitions[0] run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "At least one final state has a transition."}] \ +} +execute if data storage bs:ctx _.finals[].transitions[0] run return fail + +return 1 diff --git a/modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction index 95c68cf6d..c3376125d 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/initiality.mcfunction @@ -1,3 +1,14 @@ # Input: # Storage: bs:ctx _.fsm (a FSM) -return run function bs.fsm:check/internal/initiality with storage bs:ctx _.fsm + +# Goal: check if the specified initial state exist in the FSM +execute store success score #s bs.ctx run function bs.fsm:check/internal/initiality with storage bs:ctx _.fsm +execute if score #s bs.ctx matches 0 run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "The initial state does not exist in the FSM."}] \ +} +execute if score #s bs.ctx matches 0 run return fail + +return 1 diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction index 8354e4ce1..f468af345 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction @@ -3,5 +3,21 @@ # Storage: bs:ctx _.states_to_find (a list of states name) # Storage: bs:ctx _.fsm (a FSM) +# If we have no more state to browse, we return +execute unless data storage bs:ctx _.states_to_browse[-1] run return 1 + +# We select the first state of the stack to get states having a transition to it data modify storage bs:ctx _.current_state set from storage bs:ctx _.states_to_browse[-1] + +# This function places the states having a transition to the current state in storage bs:ctx _.states_to_find function bs.fsm:check/internal/get_input_states with storage bs:ctx _ + +# We remove the state from the stack +data remove storage bs:ctx _.states_to_browse[-1] + +# We update the list of states to find, if we fail, we return since it means that one of the states does not exist +# This will also add the found states to the list of states to browse +execute unless function bs.fsm:check/internal/update_found_states run return fail + +# We continue to browse the states, we return the result of the function call to propagate errors +return run function bs.fsm:check/internal/explore_state diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction index fc56fec62..20b52b0cd 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction @@ -2,5 +2,8 @@ # Macro: $(current_state) (a state name) # Storage: bs:ctx _.fsm (a FSM) +# Output: +# Storage: bs:ctx _.source_states (a list of states name) + data modify storage bs:ctx _.source_states set value [] $data modify storage bs:ctx _.source_states append from storage bs:ctx _.fsm.states[{transitions: [{to: $(current_state)}]}] diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction new file mode 100644 index 000000000..7c5fc99a9 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction @@ -0,0 +1,9 @@ +# Input: +# Macro: $(state) (a state name) +# Storage: bs:ctx _.states_to_find (a list of states name) + +# Output: +# Set the state has selected +# If no state, return fail + +$return run data modify storage bs:ctx _.states_to_find[{name: $(state)}].selected set value true diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction new file mode 100644 index 000000000..e1e6547c5 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction @@ -0,0 +1,34 @@ +# Input: +# Storage: bs:ctx _.source_states (a list of states name) +# Storage: bs:ctx _.states_to_find (a list of states name) +# Storage: bs:ctx _.states_to_browse (a list of states name) + +# If we have no more state to find, we directly return to stop the recursive loop +execute unless data storage bs:ctx _.source_states[0] run return 1 + +data modify storage bs:ctx _.state set from storage bs:ctx _.source_states[0] +data remove storage bs:ctx _.source_states[0] + +# First, we select the state to find, to avoid the use of multiple macro commands +execute store success score #s bs.ctx run function bs.fsm:check/internal/select_state with storage bs:ctx _ + +# If we fail to select the state, that is because the state does not exist, we log an error and return +execute if score #s bs.ctx matches 0 run function #bs.log:error { \ + namespace: bs.fsm, \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "The state '"}, {nbt: "_.state",storage: "bs:ctx"},{text: "' does not exist."}] \ +} +execute if score #s bs.ctx matches 0 run return fail + +# If we succeed to write the state as found, it means that the state was not found before +execute store success score #s bs.ctx run data modify storage bs:ctx _.states_to_find[{selected: true}].found set value true +data remove storage bs:ctx _.states_to_find[{selected: true}].selected + +# If we succeed, we add the state to the list of states to browse +execute if score #s bs.ctx matches 1 run data modify storage bs:ctx _.states_to_browse append from storage bs:ctx _.state + +# Else, that means we already browse the state, or we already plan to browse it. Since we do not want to browse the same state twice, we do nothing + +# We need to run this function until we have no more state to find, we directly return the result of the function call to propagate errors +return run function bs.fsm:check/internal/update_found_states diff --git a/modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction new file mode 100644 index 000000000..7c3ee32c3 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction @@ -0,0 +1,12 @@ +# Input: +# Storage: bs:ctx _.fsm (a FSM) + +execute store success score #s bs.ctx run function bs.fsm:check/initiality +execute if score #s bs.ctx matches 1 store success score #s bs.ctx run function bs.fsm:check/unicity +# Need to be call before reachability, since this latter uses the finals states +execute if score #s bs.ctx matches 1 store success score #s bs.ctx run function bs.fsm:check/acceptability +execute if score #s bs.ctx matches 1 store success score #s bs.ctx run function bs.fsm:check/reachability + +execute if score #s bs.ctx matches 0 run return fail + +return 1 diff --git a/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction index e30200caf..af457ec60 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction @@ -1,16 +1,53 @@ # Input: # Storage: bs:ctx _.fsm (a FSM) # Storage: bs:ctx _.finals (a list of states) -# Storage: bs:ctx _.initial (a state) # Storage: bs:ctx _.found_states (a list of states name) -data modify storage bs:ctx _.non_final_states set value [] -data modify storage bs:ctx _.non_final_states append from storage bs:ctx _.fsm.states[] -data remove storage bs:ctx _.non_final_states[{final: true}] +# Goal: check if from any state of the FSM, we can reach a final state. +# In more technical terms, we want to check if the FSM is a weakly connected graph with a path existing from any state to a final state. +# How do we proceed? Depth-first search algorithm +# 1. We initialize an empty stack to store the states to explore +# 1. We stack into it the final states +# 2. We unstack the first state of the stack and identify the states having a transition to it (internal/explore_state) +# 3. We add them to the stack if we have not already visited them (internal/update_found_states) +# 4. We repeat the process until the stack is empty (internal/explore_state) +# → If we visit all the states, that means that from any state of the FSM, we can reach a final state +# → Else, some states cannot reach a final state + +# We also profit from this function to check if all states used in transitions are defined in the FSM + + +# Initialization data modify storage bs:ctx _.states_to_find set value [] -data modify storage bs:ctx _.states_to_find append from storage bs:ctx _.non_final_states[].name +data modify storage bs:ctx _.states_to_find append from storage bs:ctx _.fsm.states[] +data modify storage bs:ctx _.states_to_find[].found set value false +# We set the final states as found +data modify storage bs:ctx _.states_to_find[{final: true}].found set value true +# We initialize the stack of states to start exploring with the final states data modify storage bs:ctx _.states_to_browse set value [] data modify storage bs:ctx _.states_to_browse append from storage bs:ctx _.finals[].name + +# We start the depth-first search algorithm +execute store success score #s bs.ctx run function bs.fsm:check/internal/explore_state + +# If we fail to explore the states, that means that some states used in transitions are not defined in the FSM +execute if score #s bs.ctx matches 0 run return fail + +# Else, we check if all states are found +data modify storage bs:ctx _.not_found_states set value [] +data modify storage bs:ctx _.not_found_states append from storage bs:ctx _.states_to_find[{found: false}] + +# If we have some states that cannot reach a final state, we log an error and return +execute if data storage bs:ctx _.not_found_states[0] run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "The states '"}, {nbt: "_.not_found_states", storage: "bs:ctx"},{text: "' cannot reach a final state."}] \ +} +execute if data storage bs:ctx _.not_found_states[0] run return fail + +# Else, we return success +return 1 diff --git a/modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction new file mode 100644 index 000000000..7038b1f35 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction @@ -0,0 +1,34 @@ +# Input: +# Storage: bs:ctx _.fsm (a FSM) + +# Goal: check if the names of the states are unique +# How do we proceed? +# We will use a specific behavior of Minecraft mob's tag: the list of tags cannot have duplicates +# Following that, we can compare the size of the list of tags with the size of the list of states +# If they are different, that means that there are duplicate names + +# We get the size of the list of states names +execute store result score #a bs.ctx run data get storage bs:ctx _.fsm.states +# We get the size of the list of tags to substract at the end +execute store result score #s bs.ctx run data get entity B5-0-0-0-1 Tags +# We set the list of tags to the list of states names +data modify entity B5-0-0-0-1 Tags append from storage bs:ctx _.fsm.states[].name +# We get the list of tags +execute store result score #b bs.ctx run data get entity B5-0-0-0-1 Tags +# We reset the tags to the default value +data modify entity B5-0-0-0-1 Tags set value ["bs.entity","bs.persistent","smithed.entity","smithed.strict"] + +# As our list of tags has our state names with the default tags, we need to substract the size of the list of tags before our append to the size of the list of states +scoreboard players operation #b bs.ctx -= #s bs.ctx + +# We compare the size of the list of tags with the size of the list of states, if they are different, that means that there are duplicate names so we log an error and return +execute unless score #a bs.ctx = #b bs.ctx run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:check/unicity", \ + tag: "unicity", \ + message: [{text: "The names of the states are not unique."}] \ +} +execute unless score #a bs.ctx = #b bs.ctx run return fail + +# Else, we return success +return 1 diff --git a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction index d5a5f1553..375d569ad 100644 --- a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction @@ -21,6 +21,7 @@ # ] # } +# TODO: manque la vérification de l'unicité des noms des états # Check if the FSM already exists. $execute if data storage bs:data fsm.fsm.'$(name)' run function #bs.log:error { \ @@ -31,10 +32,10 @@ $execute if data storage bs:data fsm.fsm.'$(name)' run function #bs.log:error { } $execute if data storage bs:data fsm.fsm.'$(name)' run return fail -# Check if the initial state exists. $data modify storage bs:ctx _ set value { fsm: $(fsm) } - - +# Check if the FSM is valid +execute store success score #s bs.ctx run function bs.fsm:check/is_valid +execute if score #s bs.ctx matches 0 run return fail $data modify storage bs:data fsm.fsm.'$(name)' set value $(fsm) From 2236b41101c16b9dcc18b4eb69adc8c8f7adab46 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:35:44 +0000 Subject: [PATCH 04/14] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20chore:=20update?= =?UTF-8?q?=20generated=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/manifest.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/data/manifest.json b/data/manifest.json index d134ff104..e91331034 100644 --- a/data/manifest.json +++ b/data/manifest.json @@ -751,6 +751,21 @@ } ] }, + { + "id": "bs.fsm", + "name": "Final State Machine", + "slug": "bookshelf-fsm", + "description": "Bookshelf final state machine module.", + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html", + "kind": "data_pack", + "tags": [ + "runtime" + ], + "dependencies": [ + "bs.random" + ], + "features": [] + }, { "id": "bs.generation", "name": "Generation", From 5724c2b456f8e528aafe530bbb8073e68063caa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Thu, 17 Jul 2025 18:44:22 +0200 Subject: [PATCH 05/14] Progress --- docs/modules/fsm.md | 324 ++++++++++++++++++ .../function/check/acceptability.mcfunction | 4 +- .../internal/get_input_states.mcfunction | 3 +- .../internal/update_found_states.mcfunction | 3 + .../function/check/reachability.mcfunction | 2 +- .../data/bs.fsm/function/new.mcfunction | 2 - .../bs.fsm/function/new_vanilla.mcfunction | 303 ++++++++++++++++ .../bs.fsm/data/bs.fsm/tags/function/new.json | 20 ++ .../bs.fsm/data/bs.fsm/test/new.mcfunction | 303 ++++++++++++++++ 9 files changed, 958 insertions(+), 6 deletions(-) create mode 100644 docs/modules/fsm.md create mode 100644 modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/tags/function/new.json create mode 100644 modules/bs.fsm/data/bs.fsm/test/new.mcfunction diff --git a/docs/modules/fsm.md b/docs/modules/fsm.md new file mode 100644 index 000000000..33f9f8b0b --- /dev/null +++ b/docs/modules/fsm.md @@ -0,0 +1,324 @@ +# 🔄 FSM (Finite State Machine) + +**`#bs.fsm:help`** + +A powerful Finite State Machine (FSM) system for managing complex state-based behaviors in Minecraft. + +```{epigraph} +FSMs are without a doubt the most commonly used technology in game AI programming today. +They are conceptually simple, efficient, easily extensible, and yet powerful enough to handle a wide variety of situations. + +-- Daniel D. Fu & Ryan Houlette +``` + +The FSM module provides a comprehensive system for creating, managing, and executing finite state machines. +It allows you to define states, transitions, and behaviors in a declarative way, making complex state management simple and maintainable. + +--- + +## 🔧 Functions + +You can find below all functions available in this module. + +--- + +### New + +```{function} #bs.fsm:new + +Create a new Finite State Machine (FSM) with the specified configuration. + +:Inputs: + **Function macro**: + :::{treeview} + - {nbt}`compound` Arguments + - {nbt}`string` **name**: Unique identifier for the FSM. + - {nbt}`compound` **fsm**: FSM configuration object. + - {nbt}`string` **initial**: Name of the initial state (must exist in states array). + - {nbt}`string` **on_cancel**: Function to call when the FSM is cancelled (optional). + - {nbt}`list` **states**: Array of state definitions. + - {nbt}`compound` State + - {nbt}`string` **name**: Unique name for the state. + - {nbt}`string` **on_tick**: Function to call every tick while in this state (optional). + - {nbt}`string` **on_enter**: Function to call when entering this state (optional). + - {nbt}`string` **on_exit**: Function to call when exiting this state (optional). + - {nbt}`bool` **final**: Whether this state is a final state (optional, default: false). + - {nbt}`list` **transitions**: Array of transition definitions (optional). + - {nbt}`compound` Transition + - {nbt}`string` **name**: Name of the transition (optional). + - {nbt}`string` {nbt}`compound` **condition**: Transition condition. + - **"manual"**: Manual transition triggered by external call. + - {nbt}`compound` **predicate**: Predicate-based transition. + - {nbt}`string` **type**: Must be "predicate". + - {nbt}`string` **wait**: Predicate function to evaluate. + - {nbt}`compound` **function**: Function-based transition. + - {nbt}`string` **type**: Must be "function". + - {nbt}`string` **wait**: Function to call for evaluation. + - {nbt}`compound` **hook**: Hook-based transition. + - {nbt}`string` **type**: Must be "hook". + - {nbt}`string` **wait**: Hook function to evaluate. + - {nbt}`compound` **delay**: Time-based transition. + - {nbt}`string` **type**: Must be "delay". + - {nbt}`string` **wait**: Time delay (e.g., "20t" for 1 second). + - {nbt}`string` **to**: Name of the target state (must exist in states array). + ::: + +:Outputs: + **Return**: Success (1) if FSM was created successfully, failure (0) otherwise. + + **State**: The FSM is registered and available for use. +``` + +*Example: Create a simple door FSM with open/closed states:* + +```mcfunction +# Create a door FSM +function #bs.fsm:new { \ + name: "door_fsm", \ + fsm: { \ + initial: "closed", \ + on_cancel: "bs.door:cancel", \ + states: [ \ + { \ + name: "closed", \ + on_tick: "bs.door:closed_tick", \ + on_enter: "bs.door:close_door", \ + on_exit: "bs.door:prepare_open", \ + transitions: [ \ + { \ + name: "open", \ + condition: "manual", \ + to: "opening" \ + } \ + ] \ + }, \ + { \ + name: "opening", \ + on_tick: "bs.door:opening_tick", \ + on_enter: "bs.door:start_opening", \ + on_exit: "bs.door:finish_opening", \ + transitions: [ \ + { \ + name: "opened", \ + condition: { type: "delay", wait: "20t" }, \ + to: "open" \ + } \ + ] \ + }, \ + { \ + name: "open", \ + on_tick: "bs.door:open_tick", \ + on_enter: "bs.door:open_door", \ + on_exit: "bs.door:prepare_close", \ + transitions: [ \ + { \ + name: "close", \ + condition: "manual", \ + to: "closing" \ + } \ + ] \ + }, \ + { \ + name: "closing", \ + on_tick: "bs.door:closing_tick", \ + on_enter: "bs.door:start_closing", \ + on_exit: "bs.door:finish_closing", \ + transitions: [ \ + { \ + name: "closed", \ + condition: { type: "delay", wait: "20t" }, \ + to: "closed" \ + } \ + ] \ + } \ + ] \ + } \ +} +``` + +> **Credits**: theogiraudet + +--- + +### Start + +```{function} #bs.fsm:start + +Start a new instance of a Finite State Machine. + +:Inputs: + **Function macro**: + :::{treeview} + - {nbt}`compound` Arguments + - {nbt}`string` **fsm_name**: Name of the FSM to instantiate (must exist). + - {nbt}`string` **instance_name**: Unique identifier for this FSM instance. + - {nbt}`string` **bind**: Binding type for the instance. + - **"global"**: Instance is bound globally and accessible from anywhere. + - **"local"**: Instance is bound to the current execution context. + ::: + +:Outputs: + **Return**: Success (1) if instance was started successfully, failure (0) otherwise. + + **State**: The FSM instance is created and begins execution in its initial state. +``` + +*Example: Start a door FSM instance:* + +```mcfunction +# Start a door FSM instance +function #bs.fsm:start { fsm_name: "door_fsm", instance_name: "main_door", bind: "global" } + +# The door FSM is now running and will execute its initial state +``` + +> **Credits**: theogiraudet + +--- + +### Cancel + +```{function} #bs.fsm:cancel + +Cancel and stop a running FSM instance. + +:Inputs: + **Function macro**: + :::{treeview} + - {nbt}`compound` Arguments + - {nbt}`string` **instance_name**: Name of the FSM instance to cancel. + - {nbt}`string` **bind**: Binding type of the instance. + - **"global"**: Instance is bound globally. + - **"local"**: Instance is bound to the current execution context. + ::: + +:Outputs: + **Return**: Success (1) if instance was cancelled successfully, failure (0) otherwise. + + **State**: The FSM instance is stopped and cleaned up. If the FSM has an on_cancel function, it will be called. +``` + +*Example: Cancel a door FSM instance:* + +```mcfunction +# Cancel the door FSM instance +function #bs.fsm:cancel { instance_name: "main_door", bind: "global" } + +# The door FSM instance is now stopped +``` + +> **Credits**: theogiraudet + +--- + +### Delete + +```{function} #bs.fsm:delete + +Delete a Finite State Machine definition and all its instances. + +:Inputs: + **Function macro**: + :::{treeview} + - {nbt}`compound` Arguments + - {nbt}`string` **fsm_name**: Name of the FSM to delete. + ::: + +:Outputs: + **Return**: Success (1) if FSM was deleted successfully, failure (0) otherwise. + + **State**: The FSM definition and all its running instances are removed. +``` + +*Example: Delete a door FSM:* + +```mcfunction +# Delete the door FSM +function #bs.fsm:delete { fsm_name: "door_fsm" } + +# The door FSM and all its instances are now removed +``` + +> **Credits**: theogiraudet + +--- + +## 📋 Validation Rules + +The FSM system enforces several validation rules to ensure proper operation: + +### Initiality +- The FSM must have an `initial` state specified +- The initial state must exist in the states array + +### Unicity +- All state names must be unique within the FSM +- All transition names must be unique within a state (if specified) + +### Acceptability +- The FSM must have at least one final state +- Final states are states marked with `final: true` + +### Reachability +- All final states must be reachable from the initial state +- This is determined by analyzing the transition graph + +### Transition Validation +- All transition target states must exist in the states array +- Transition conditions must be valid according to their type + +--- + +## 🔄 State Lifecycle + +Each state in an FSM follows a specific lifecycle: + +1. **Enter**: The `on_enter` function is called when entering the state +2. **Tick**: The `on_tick` function is called every tick while in the state +3. **Transition**: When a transition condition is met, the state transitions +4. **Exit**: The `on_exit` function is called when leaving the state + +--- + +## ⚡ Transition Types + +The FSM system supports several types of transitions: + +### Manual +Triggered by external function calls. Useful for player interactions or external events. + +### Predicate +Triggered when a predicate function returns true. Useful for conditional logic. + +### Function +Triggered when a function returns a specific value. Useful for complex conditions. + +### Hook +Triggered by hook system events. Useful for integration with other systems. + +### Delay +Triggered after a specified time delay. Useful for timed behaviors. + +--- + +## 🎯 Use Cases + +FSMs are particularly useful for: + +- **Entity AI**: Managing complex behavior patterns +- **Machine States**: Controlling redstone contraptions +- **Game Mechanics**: Implementing complex game logic +- **UI Systems**: Managing interface states +- **Animation Systems**: Controlling entity animations +- **Quest Systems**: Managing quest progression + +--- + +## ⚠️ Best Practices + +1. **Keep states focused**: Each state should represent a single, well-defined behavior +2. **Use meaningful names**: State and transition names should clearly describe their purpose +3. **Handle edge cases**: Always consider what happens when transitions fail +4. **Clean up resources**: Use the on_exit functions to clean up state-specific resources +5. **Test thoroughly**: FSMs can become complex, so comprehensive testing is essential +6. **Document transitions**: Clearly document when and why transitions occur diff --git a/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction index 02fc5c176..8ceb2e15c 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/acceptability.mcfunction @@ -10,13 +10,13 @@ data modify storage bs:ctx _.finals set value [] data modify storage bs:ctx _.finals append from storage bs:ctx _.fsm.states[{final: true}] -execute if data storage bs:ctx _.finals[0] run function #bs.log:error { \ +execute unless data storage bs:ctx _.finals[0] run function #bs.log:error { \ namespace: "bs.fsm", \ path: "#bs.fsm:new", \ tag: "new", \ message: [{text: "The FSM has no final state."}] \ } -execute if data storage bs:ctx _.finals[0] run return fail +execute unless data storage bs:ctx _.finals[0] run return fail execute if data storage bs:ctx _.finals[].transitions[0] run function #bs.log:error { \ namespace: "bs.fsm", \ diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction index 20b52b0cd..56b5be6b9 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction @@ -6,4 +6,5 @@ # Storage: bs:ctx _.source_states (a list of states name) data modify storage bs:ctx _.source_states set value [] -$data modify storage bs:ctx _.source_states append from storage bs:ctx _.fsm.states[{transitions: [{to: $(current_state)}]}] +$tellraw @a [{text: "current_state: "}, {text: "$(current_state)"}] +$data modify storage bs:ctx _.source_states append from storage bs:ctx _.fsm.states[{transitions: [{to: "$(current_state)"}]}] diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction index e1e6547c5..aa2a24df8 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction @@ -21,6 +21,9 @@ execute if score #s bs.ctx matches 0 run function #bs.log:error { \ } execute if score #s bs.ctx matches 0 run return fail +# display selected state +tellraw @a [{text: "selected state: "}, {nbt: "_.states_to_find[{selected: true}].name",storage: "bs:ctx"}] + # If we succeed to write the state as found, it means that the state was not found before execute store success score #s bs.ctx run data modify storage bs:ctx _.states_to_find[{selected: true}].found set value true data remove storage bs:ctx _.states_to_find[{selected: true}].selected diff --git a/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction index af457ec60..93cf86198 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/reachability.mcfunction @@ -45,7 +45,7 @@ execute if data storage bs:ctx _.not_found_states[0] run function #bs.log:error namespace: "bs.fsm", \ path: "#bs.fsm:new", \ tag: "new", \ - message: [{text: "The states '"}, {nbt: "_.not_found_states", storage: "bs:ctx"},{text: "' cannot reach a final state."}] \ + message: [{text: "The states '"}, {nbt: "_.not_found_states[].name", storage: "bs:ctx"},{text: "' cannot reach a final state."}] \ } execute if data storage bs:ctx _.not_found_states[0] run return fail diff --git a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction index 375d569ad..32a4c1aa8 100644 --- a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction @@ -21,8 +21,6 @@ # ] # } -# TODO: manque la vérification de l'unicité des noms des états - # Check if the FSM already exists. $execute if data storage bs:data fsm.fsm.'$(name)' run function #bs.log:error { \ namespace: bs.fsm, \ diff --git a/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction new file mode 100644 index 000000000..fa0981b7f --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction @@ -0,0 +1,303 @@ +# ------------------------------------------------------------------------------------------------------------ +# Copyright (c) 2025 Gunivers +# +# This file is part of the Bookshelf project (https://github.com/mcbookshelf/bookshelf). +# +# This source code is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Conditions: +# - You may use this file in compliance with the MPL v2.0 +# - Any modifications must be documented and disclosed under the same license +# +# For more details, refer to the MPL v2.0. +# ------------------------------------------------------------------------------------------------------------ + +## === SETUP === + +# Clear any existing FSM data +data remove storage bs:data fsm.fsm + +## === VALID FSM CREATION === + +# Test 1: Create a simple valid FSM +function #bs.fsm:new { \ + name: "test_fsm", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "start", \ + condition: "manual", \ + to: "active" \ + } \ + ] \ + }, \ + { \ + name: "active", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ + } \ + ] \ + } \ +} +execute unless data storage bs:data fsm.fsm.test_fsm run say "Failed to create a valid FSM" + +# ## === DUPLICATE FSM ERROR === + +# # Test 2: Try to create the same FSM again (should fail) +# execute store success score #s bs.ctx run function #bs.fsm:new { \ +# name: "test_fsm", \ +# fsm: { \ +# initial: "idle", \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit" \ +# } \ +# ] \ +# } \ +# } +# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when creating a duplicate FSM" + +# ## === INVALID FSM - MISSING INITIAL STATE === + +# # Test 3: Create FSM with missing initial state +# execute store success score #s bs.ctx run function #bs.fsm:new { \ +# name: "invalid_fsm_1", \ +# fsm: { \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit" \ +# } \ +# ] \ +# } \ +# } +# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has no initial state" + +# ## === INVALID FSM - INITIAL STATE NOT FOUND === + +# # Test 4: Create FSM with initial state that doesn't exist +# execute store success score #s bs.ctx run function #bs.fsm:new { \ +# name: "invalid_fsm_2", \ +# fsm: { \ +# initial: "nonexistent", \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit" \ +# } \ +# ] \ +# } \ +# } +# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when initial state doesn't exist" + +# ## === INVALID FSM - DUPLICATE STATE NAMES === + +# # Test 5: Create FSM with duplicate state names +# execute store success score #s bs.ctx run function #bs.fsm:new { \ +# name: "invalid_fsm_3", \ +# fsm: { \ +# initial: "idle", \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit" \ +# }, \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit" \ +# } \ +# ] \ +# } \ +# } +# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has duplicate state names" + +# ## === INVALID FSM - TRANSITION TO NONEXISTENT STATE === + +# # Test 6: Create FSM with transition to non-existent state +# execute store success score #s bs.ctx run function #bs.fsm:new { \ +# name: "invalid_fsm_4", \ +# fsm: { \ +# initial: "idle", \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit", \ +# transitions: [ \ +# { \ +# name: "start", \ +# condition: "manual", \ +# to: "nonexistent" \ +# } \ +# ] \ +# } \ +# ] \ +# } \ +# } +# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when transition points to non-existent state" + +# ## === VALID FSM WITH COMPLEX TRANSITIONS === + +# # Test 7: Create a valid FSM with different transition types +# function #bs.fsm:new { \ +# name: "complex_fsm", \ +# fsm: { \ +# initial: "start", \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "start", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit", \ +# transitions: [ \ +# { \ +# name: "manual_transition", \ +# condition: "manual", \ +# to: "waiting" \ +# }, \ +# { \ +# name: "predicate_transition", \ +# condition: { type: "predicate", wait: "bs.fsm:test/condition" }, \ +# to: "processing" \ +# }, \ +# { \ +# name: "function_transition", \ +# condition: { type: "function", wait: "bs.fsm:test/function" }, \ +# to: "processing" \ +# }, \ +# { \ +# name: "hook_transition", \ +# condition: { type: "hook", wait: "bs.fsm:test/hook" }, \ +# to: "processing" \ +# }, \ +# { \ +# name: "delay_transition", \ +# condition: { type: "delay", wait: "20t" }, \ +# to: "processing" \ +# } \ +# ] \ +# }, \ +# { \ +# name: "waiting", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit" \ +# }, \ +# { \ +# name: "processing", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit", \ +# final: true \ +# } \ +# ] \ +# } \ +# } +# execute unless data storage bs:data fsm.fsm.complex_fsm run say "Failed to create a complex FSM with various transition types" + +# ## === VALID FSM WITH MINIMAL CONFIGURATION === + +# # Test 8: Create a minimal valid FSM +# function #bs.fsm:new { \ +# name: "minimal_fsm", \ +# fsm: { \ +# initial: "state1", \ +# states: [ \ +# { \ +# name: "state1", \ +# final: true \ +# } \ +# ] \ +# } \ +# } +# execute unless data storage bs:data fsm.fsm.minimal_fsm run say "Failed to create a minimal FSM" + +# ## === INVALID FSM - UNREACHABLE FINAL STATE === + +# # Test 9: Create FSM with unreachable final state +# execute store success score #s bs.ctx run function #bs.fsm:new { \ +# name: "invalid_fsm_5", \ +# fsm: { \ +# initial: "idle", \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit", \ +# final: false \ +# }, \ +# { \ +# name: "final_state", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit", \ +# final: true \ +# } \ +# ] \ +# } \ +# } +# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has unreachable final state" + +# ## === INVALID FSM - NO FINAL STATE === + +# # Test 10: Create FSM with no final state +# execute store success score #s bs.ctx run function #bs.fsm:new { \ +# name: "invalid_fsm_6", \ +# fsm: { \ +# initial: "idle", \ +# on_cancel: "bs.fsm:test/cancel", \ +# states: [ \ +# { \ +# name: "idle", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit", \ +# final: false \ +# }, \ +# { \ +# name: "active", \ +# on_tick: "bs.fsm:test/tick", \ +# on_enter: "bs.fsm:test/enter", \ +# on_exit: "bs.fsm:test/exit", \ +# final: false \ +# } \ +# ] \ +# } \ +# } +# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has no final state" + +# ## === CLEANUP === + +# # Clean up test data +# data remove storage bs:data fsm.fsm diff --git a/modules/bs.fsm/data/bs.fsm/tags/function/new.json b/modules/bs.fsm/data/bs.fsm/tags/function/new.json new file mode 100644 index 000000000..44fffeb93 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/tags/function/new.json @@ -0,0 +1,20 @@ +{ + "__bookshelf__": { + "feature": true, + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#new", + "authors": [ + "theogiraudet" + ], + "created": { + "date": "2025/07/16", + "minecraft_version": "1.21.7" + }, + "updated": { + "date": "2025/07/16", + "minecraft_version": "1.21.7" + } + }, + "values": [ + "bs.fsm:new" + ] +} diff --git a/modules/bs.fsm/data/bs.fsm/test/new.mcfunction b/modules/bs.fsm/data/bs.fsm/test/new.mcfunction new file mode 100644 index 000000000..3e6eca795 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/test/new.mcfunction @@ -0,0 +1,303 @@ +# ------------------------------------------------------------------------------------------------------------ +# Copyright (c) 2025 Gunivers +# +# This file is part of the Bookshelf project (https://github.com/mcbookshelf/bookshelf). +# +# This source code is subject to the terms of the Mozilla Public License, v. 2.0. +# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# Conditions: +# - You may use this file in compliance with the MPL v2.0 +# - Any modifications must be documented and disclosed under the same license +# +# For more details, refer to the MPL v2.0. +# ------------------------------------------------------------------------------------------------------------ + +## === SETUP === + +# Clear any existing FSM data +data remove storage bs:data fsm.fsm + +## === VALID FSM CREATION === + +# Test 1: Create a simple valid FSM +function #bs.fsm:new { \ + name: "test_fsm", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "start", \ + condition: "manual", \ + to: "active" \ + } \ + ] \ + }, \ + { \ + name: "active", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ + } \ + ] \ + } \ +} +execute unless data storage bs:data fsm.fsm.test_fsm run fail "Failed to create a valid FSM" + +## === DUPLICATE FSM ERROR === + +# Test 2: Try to create the same FSM again (should fail) +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "test_fsm", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run fail "Failed to return an error when creating a duplicate FSM" + +## === INVALID FSM - MISSING INITIAL STATE === + +# Test 3: Create FSM with missing initial state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_1", \ + fsm: { \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run fail "Failed to return an error when FSM has no initial state" + +## === INVALID FSM - INITIAL STATE NOT FOUND === + +# Test 4: Create FSM with initial state that doesn't exist +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_2", \ + fsm: { \ + initial: "nonexistent", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run fail "Failed to return an error when initial state doesn't exist" + +## === INVALID FSM - DUPLICATE STATE NAMES === + +# Test 5: Create FSM with duplicate state names +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_3", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + }, \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run fail "Failed to return an error when FSM has duplicate state names" + +## === INVALID FSM - TRANSITION TO NONEXISTENT STATE === + +# Test 6: Create FSM with transition to non-existent state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_4", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "start", \ + condition: "manual", \ + to: "nonexistent" \ + } \ + ] \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run fail "Failed to return an error when transition points to non-existent state" + +## === VALID FSM WITH COMPLEX TRANSITIONS === + +# Test 7: Create a valid FSM with different transition types +function #bs.fsm:new { \ + name: "complex_fsm", \ + fsm: { \ + initial: "start", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "start", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "manual_transition", \ + condition: "manual", \ + to: "waiting" \ + }, \ + { \ + name: "predicate_transition", \ + condition: { type: "predicate", wait: "bs.fsm:test/condition" }, \ + to: "processing" \ + }, \ + { \ + name: "function_transition", \ + condition: { type: "function", wait: "bs.fsm:test/function" }, \ + to: "processing" \ + }, \ + { \ + name: "hook_transition", \ + condition: { type: "hook", wait: "bs.fsm:test/hook" }, \ + to: "processing" \ + }, \ + { \ + name: "delay_transition", \ + condition: { type: "delay", wait: "20t" }, \ + to: "processing" \ + } \ + ] \ + }, \ + { \ + name: "waiting", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + }, \ + { \ + name: "processing", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ + } \ + ] \ + } \ +} +execute unless data storage bs:data fsm.fsm.complex_fsm run fail "Failed to create a complex FSM with various transition types" + +## === VALID FSM WITH MINIMAL CONFIGURATION === + +# Test 8: Create a minimal valid FSM +function #bs.fsm:new { \ + name: "minimal_fsm", \ + fsm: { \ + initial: "state1", \ + states: [ \ + { \ + name: "state1", \ + final: true \ + } \ + ] \ + } \ +} +execute unless data storage bs:data fsm.fsm.minimal_fsm run fail "Failed to create a minimal FSM" + +## === INVALID FSM - UNREACHABLE FINAL STATE === + +# Test 9: Create FSM with unreachable final state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_5", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: false \ + }, \ + { \ + name: "final_state", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run fail "Failed to return an error when FSM has unreachable final state" + +## === INVALID FSM - NO FINAL STATE === + +# Test 10: Create FSM with no final state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_6", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: false \ + }, \ + { \ + name: "active", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: false \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run fail "Failed to return an error when FSM has no final state" + +## === CLEANUP === + +# Clean up test data +data remove storage bs:data fsm.fsm From 65c0e3f6038965800727f459577ee00437d8f741 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 16:45:17 +0000 Subject: [PATCH 06/14] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20chore:=20update?= =?UTF-8?q?=20generated=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/manifest.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/data/manifest.json b/data/manifest.json index e91331034..0cc20256b 100644 --- a/data/manifest.json +++ b/data/manifest.json @@ -764,7 +764,23 @@ "dependencies": [ "bs.random" ], - "features": [] + "features": [ + { + "id": "#bs.fsm:new", + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#new", + "authors": [ + "theogiraudet" + ], + "created": { + "date": "2025/07/16", + "minecraft_version": "1.21.7" + }, + "updated": { + "date": "2025/07/16", + "minecraft_version": "1.21.7" + } + } + ] }, { "id": "bs.generation", From 541f96ef6a0f36a7d490aae4acdfab9378df10f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Thu, 17 Jul 2025 23:00:47 +0200 Subject: [PATCH 07/14] Progress --- .../check/internal/explore_state.mcfunction | 2 +- .../internal/get_input_states.mcfunction | 1 - .../check/internal/initiality.mcfunction | 2 + .../check/internal/select_state.mcfunction | 2 +- .../internal/update_found_states.mcfunction | 7 +- .../internal/well_formedness_state.mcfunction | 28 + .../well_formedness_transition.mcfunction | 85 ++++ .../bs.fsm/function/check/is_valid.mcfunction | 8 +- .../bs.fsm/function/check/unicity.mcfunction | 6 +- .../function/check/well_formedness.mcfunction | 41 ++ .../data/bs.fsm/function/new.mcfunction | 9 +- .../bs.fsm/function/new_vanilla.mcfunction | 481 +++++++++--------- .../data/bs.fsm/function/start.mcfunction | 26 + .../bs.fsm/data/bs.fsm/test/new.mcfunction | 21 +- 14 files changed, 472 insertions(+), 247 deletions(-) create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_state.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/check/well_formedness.mcfunction diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction index f468af345..cb984b740 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/explore_state.mcfunction @@ -15,7 +15,7 @@ function bs.fsm:check/internal/get_input_states with storage bs:ctx _ # We remove the state from the stack data remove storage bs:ctx _.states_to_browse[-1] -# We update the list of states to find, if we fail, we return since it means that one of the states does not exist +# We update the list of states to find, if we fail, we return since it means that one of the states does not exist (even if this is supposed to be impossible) # This will also add the found states to the list of states to browse execute unless function bs.fsm:check/internal/update_found_states run return fail diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction index 56b5be6b9..7ff1cda4e 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/get_input_states.mcfunction @@ -6,5 +6,4 @@ # Storage: bs:ctx _.source_states (a list of states name) data modify storage bs:ctx _.source_states set value [] -$tellraw @a [{text: "current_state: "}, {text: "$(current_state)"}] $data modify storage bs:ctx _.source_states append from storage bs:ctx _.fsm.states[{transitions: [{to: "$(current_state)"}]}] diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction index 44f252efe..e8b930a2a 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/initiality.mcfunction @@ -7,4 +7,6 @@ # Return 0 or 1 (0 if the state does not exist, 1 if it does) data remove storage bs:ctx _.initial +# We save the initial property directly in the state object +$data modify storage bs:ctx _.fsm.states[{name: $(initial)}].initial set value true $return run data modify storage bs:ctx _.initial set from storage bs:ctx _.fsm.states[{name: $(initial)}] diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction index 7c5fc99a9..4b264b6e2 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/select_state.mcfunction @@ -6,4 +6,4 @@ # Set the state has selected # If no state, return fail -$return run data modify storage bs:ctx _.states_to_find[{name: $(state)}].selected set value true +$return run data modify storage bs:ctx _.states_to_find[{name: '$(state)'}].selected set value true diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction index aa2a24df8..0befa94ed 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/update_found_states.mcfunction @@ -6,13 +6,13 @@ # If we have no more state to find, we directly return to stop the recursive loop execute unless data storage bs:ctx _.source_states[0] run return 1 -data modify storage bs:ctx _.state set from storage bs:ctx _.source_states[0] +data modify storage bs:ctx _.state set from storage bs:ctx _.source_states[0].name data remove storage bs:ctx _.source_states[0] # First, we select the state to find, to avoid the use of multiple macro commands execute store success score #s bs.ctx run function bs.fsm:check/internal/select_state with storage bs:ctx _ -# If we fail to select the state, that is because the state does not exist, we log an error and return +# If we fail to select the state, that is because the state does not exist (even if this is supposed to be impossible), we log an error and return execute if score #s bs.ctx matches 0 run function #bs.log:error { \ namespace: bs.fsm, \ path: "#bs.fsm:new", \ @@ -21,9 +21,6 @@ execute if score #s bs.ctx matches 0 run function #bs.log:error { \ } execute if score #s bs.ctx matches 0 run return fail -# display selected state -tellraw @a [{text: "selected state: "}, {nbt: "_.states_to_find[{selected: true}].name",storage: "bs:ctx"}] - # If we succeed to write the state as found, it means that the state was not found before execute store success score #s bs.ctx run data modify storage bs:ctx _.states_to_find[{selected: true}].found set value true data remove storage bs:ctx _.states_to_find[{selected: true}].selected diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_state.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_state.mcfunction new file mode 100644 index 000000000..253522dc5 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_state.mcfunction @@ -0,0 +1,28 @@ +# Input: +# Storage: bs:ctx _.states (a list of states) +# Entity: B5-0-0-0-1 Tags (the list of all states names) + +# Output: +# Fail if the current state is not valid + +# If we don't have any state to check, we return +execute unless data storage bs:ctx _.states[0] run return 1 + +# We check if the current state has a name +execute unless data storage bs:ctx _.states[0].name run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "A state does not have a name."}] \ +} +execute unless data storage bs:ctx _.states[0].name run return fail + +# If the state has transitions, we check if they are valid +execute if data storage bs:ctx _.states[0].transitions store success score #s bs.ctx run function bs.fsm:check/internal/well_formedness_transition + +# We propagate the error if the transitions are not valid +execute if score #s bs.ctx matches 0 run return fail + +# We check the next state +data remove storage bs:ctx _.states[0] +return run function bs.fsm:check/internal/well_formedness_state diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction new file mode 100644 index 000000000..57b337d47 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction @@ -0,0 +1,85 @@ +# Input: +# Storage: bs:ctx _.states (a list of states) +# Entity: B5-0-0-0-1 Tags (the list of all states names) + +# Output: +# Fail if the current transition is not valid + +execute unless data storage bs:ctx _.states[0].transitions[0] run return 1 + +# First, we have if the 'to' state exists +# For that, we will use the entity Tags array property which cannot store multiple occurrences of the same value +# So if we add inside the name of all the states, then the 'to' state, the size of the array will not change +data modify storage bs:ctx _.tags set from entity B5-0-0-0-1 Tags + +execute store result score #a bs.ctx run data get entity B5-0-0-0-1 Tags + +data modify entity B5-0-0-0-1 Tags append from storage bs:ctx _.states[0].transitions[0].to +execute store result score #b bs.ctx run data get entity B5-0-0-0-1 Tags + +# We restore the previous tags +data modify entity B5-0-0-0-1 Tags set from storage bs:ctx _.tags + +execute unless score #a bs.ctx = #b bs.ctx run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "A transition of '"}, {nbt: "_.states[0].name", storage: "bs:ctx"}, {text: "' refers an unknown state: "}, {nbt: "_.states[0].transitions[0].to", storage: "bs:ctx"}] \ +} +execute unless score #a bs.ctx = #b bs.ctx run return fail + + +# We check if the transition has a condition +execute unless data storage bs:ctx _.states[0].transitions[0].condition run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "A transition of '"}, {nbt: "_.states[0].name", storage: "bs:ctx"}, {text: "' does not have a condition."}] \ +} +execute unless data storage bs:ctx _.states[0].transitions[0].condition run return fail + +# We check if the condition is "manual" +data modify storage bs:ctx _.condition set value "manual" +execute store success score #s bs.ctx run data modify storage bs:ctx _.condition set from storage bs:ctx _.states[0].transitions[0].condition +# If we fail to overwrite "manual", it means that the condition is "manual" so we can return +execute if score #s bs.ctx matches 0 run return 1 + +# If the condition is not "manual", we need to check if the condition is an object +execute store success score #s bs.ctx run data modify storage bs:ctx _.condition merge value {test: true} +execute if score #s bs.ctx matches 0 run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "A transition of '"}, {nbt: "_.states[0].name", storage: "bs:ctx"}, {text: "' has an invalid condition."}] \ +} +execute if score #s bs.ctx matches 0 run return fail + +# Now, we need to check the validity of the condition object, notably the wait +execute unless data storage bs:ctx _.states[0].transitions[0].condition.wait run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "A transition of '"}, {nbt: "_.states[0].name", storage: "bs:ctx"}, {text: "' does not have a wait in its condition."}] \ +} +execute unless data storage bs:ctx _.states[0].transitions[0].condition.wait run return fail + +# We check if the condition type is valid +data modify storage bs:ctx _.condition set value [] +data modify storage bs:ctx _.condition append from storage bs:ctx _.states[0].transitions[0].condition + +execute store success score #s bs.ctx unless data storage bs:ctx _.condition[{type: "predicate"}] \ +unless data storage bs:ctx _.condition[{type: "function"}] \ +unless data storage bs:ctx _.condition[{type: "hook"}] \ +unless data storage bs:ctx _.condition[{type: "delay"}] + +execute if score #s bs.ctx matches 0 run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "A transition of '"}, {nbt: "_.states[0].name", storage: "bs:ctx"}, {text: "' has an invalid condition."}] \ +} +execute if score #s bs.ctx matches 0 run return fail + +# If we pass all the checks, we can continue to the next transition +data remove storage bs:ctx _.states[0].transitions[0] +return run function bs.fsm:check/internal/well_formedness_transition diff --git a/modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction index 7c3ee32c3..4ba12d22a 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/is_valid.mcfunction @@ -1,7 +1,13 @@ # Input: # Storage: bs:ctx _.fsm (a FSM) -execute store success score #s bs.ctx run function bs.fsm:check/initiality +# Missing checks: +# - Check the structural validity of the FSM +# - Check if all transitions refer to existing states + +execute store success score #s bs.ctx run function bs.fsm:check/well_formedness +# Also save the initial property in the initial state object +execute if score #s bs.ctx matches 1 run execute store success score #s bs.ctx run function bs.fsm:check/initiality execute if score #s bs.ctx matches 1 store success score #s bs.ctx run function bs.fsm:check/unicity # Need to be call before reachability, since this latter uses the finals states execute if score #s bs.ctx matches 1 store success score #s bs.ctx run function bs.fsm:check/acceptability diff --git a/modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction index 7038b1f35..88f4ac882 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/unicity.mcfunction @@ -7,6 +7,8 @@ # Following that, we can compare the size of the list of tags with the size of the list of states # If they are different, that means that there are duplicate names +data modify storage bs:ctx _.tags set from entity B5-0-0-0-1 Tags + # We get the size of the list of states names execute store result score #a bs.ctx run data get storage bs:ctx _.fsm.states # We get the size of the list of tags to substract at the end @@ -15,8 +17,8 @@ execute store result score #s bs.ctx run data get entity B5-0-0-0-1 Tags data modify entity B5-0-0-0-1 Tags append from storage bs:ctx _.fsm.states[].name # We get the list of tags execute store result score #b bs.ctx run data get entity B5-0-0-0-1 Tags -# We reset the tags to the default value -data modify entity B5-0-0-0-1 Tags set value ["bs.entity","bs.persistent","smithed.entity","smithed.strict"] +# We reset the tags to the default tags +data modify entity B5-0-0-0-1 Tags set from storage bs:ctx _.tags # As our list of tags has our state names with the default tags, we need to substract the size of the list of tags before our append to the size of the list of states scoreboard players operation #b bs.ctx -= #s bs.ctx diff --git a/modules/bs.fsm/data/bs.fsm/function/check/well_formedness.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/well_formedness.mcfunction new file mode 100644 index 000000000..35bd80d88 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/check/well_formedness.mcfunction @@ -0,0 +1,41 @@ +# Input: +# Storage: bs:ctx _.fsm (a FSM) + +# We copy the states names to the entity tag since an entity tag array can only store one occurrence of a value +# This array will be useful to know if a transition refers to a state that does not exist +scoreboard players set #r bs.ctx 1 +data modify storage bs:ctx _.saved_tags set from entity B5-0-0-0-1 Tags +data modify entity B5-0-0-0-1 Tags append from storage bs:ctx _.fsm.states[].name + +data modify storage bs:ctx _.states set value [] +data modify storage bs:ctx _.states append from storage bs:ctx _.fsm.states[] + +# We check if the FSM is valid + +# First, we check if the FSM has an initial state +execute unless data storage bs:ctx _.fsm.initial run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "The FSM does not have an initial state."}] \ +} +execute unless data storage bs:ctx _.fsm.initial run scoreboard players set #r bs.ctx 0 + +# Then, we check if the FSM has at least one state +execute unless data storage bs:ctx _.states[0] run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:new", \ + tag: "new", \ + message: [{text: "The FSM does not have any state."}] \ +} +execute unless data storage bs:ctx _.states[0] run scoreboard players set #r bs.ctx 0 + +# Finally, we have to check each state +execute unless score #r bs.ctx matches 0 store success score #r bs.ctx run function bs.fsm:check/internal/well_formedness_state + +# We restore the previous tags +data modify entity B5-0-0-0-1 Tags set from storage bs:ctx _.saved_tags + +# We return the result of the check +execute if score #r bs.ctx matches 0 run return fail +return 1 diff --git a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction index 32a4c1aa8..7b314373e 100644 --- a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction @@ -2,13 +2,13 @@ # Macro: name: string # Macro: fsm: { # initial: state -# on_cancel: function +# on_cancel?: function # states: [ # { # name: string -# on_tick: function -# on_exit: function -# on_enter: function +# on_tick?: function +# on_exit?: function +# on_enter?: function # final?: boolean # transitions?: [ # { @@ -37,3 +37,4 @@ execute store success score #s bs.ctx run function bs.fsm:check/is_valid execute if score #s bs.ctx matches 0 run return fail $data modify storage bs:data fsm.fsm.'$(name)' set value $(fsm) +data remove storage bs:ctx _ diff --git a/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction index fa0981b7f..064e2037d 100644 --- a/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction @@ -50,254 +50,273 @@ function #bs.fsm:new { \ ] \ } \ } -execute unless data storage bs:data fsm.fsm.test_fsm run say "Failed to create a valid FSM" +execute unless data storage bs:data fsm.fsm.test_fsm run say "Test 1: Failed to create a valid FSM" -# ## === DUPLICATE FSM ERROR === +## === DUPLICATE FSM ERROR === -# # Test 2: Try to create the same FSM again (should fail) -# execute store success score #s bs.ctx run function #bs.fsm:new { \ -# name: "test_fsm", \ -# fsm: { \ -# initial: "idle", \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit" \ -# } \ -# ] \ -# } \ -# } -# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when creating a duplicate FSM" +# Test 2: Try to create the same FSM again (should fail) +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "test_fsm", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run say "Test 2: Failed to return an error when creating a duplicate FSM" -# ## === INVALID FSM - MISSING INITIAL STATE === +## === INVALID FSM - MISSING INITIAL STATE === -# # Test 3: Create FSM with missing initial state -# execute store success score #s bs.ctx run function #bs.fsm:new { \ -# name: "invalid_fsm_1", \ -# fsm: { \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit" \ -# } \ -# ] \ -# } \ -# } -# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has no initial state" +# Test 3: Create FSM with missing initial state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_1", \ + fsm: { \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run say "Test 3: Failed to return an error when FSM has no initial state" -# ## === INVALID FSM - INITIAL STATE NOT FOUND === +## === INVALID FSM - INITIAL STATE NOT FOUND === -# # Test 4: Create FSM with initial state that doesn't exist -# execute store success score #s bs.ctx run function #bs.fsm:new { \ -# name: "invalid_fsm_2", \ -# fsm: { \ -# initial: "nonexistent", \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit" \ -# } \ -# ] \ -# } \ -# } -# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when initial state doesn't exist" +# Test 4: Create FSM with initial state that doesn't exist +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_2", \ + fsm: { \ + initial: "nonexistent", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run say "Test 4: Failed to return an error when initial state doesn't exist" -# ## === INVALID FSM - DUPLICATE STATE NAMES === +## === INVALID FSM - DUPLICATE STATE NAMES === -# # Test 5: Create FSM with duplicate state names -# execute store success score #s bs.ctx run function #bs.fsm:new { \ -# name: "invalid_fsm_3", \ -# fsm: { \ -# initial: "idle", \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit" \ -# }, \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit" \ -# } \ -# ] \ -# } \ -# } -# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has duplicate state names" +# Test 5: Create FSM with duplicate state names +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_3", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + }, \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit" \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run say "Test 5: Failed to return an error when FSM has duplicate state names" -# ## === INVALID FSM - TRANSITION TO NONEXISTENT STATE === +## === INVALID FSM - TRANSITION TO NONEXISTENT STATE === -# # Test 6: Create FSM with transition to non-existent state -# execute store success score #s bs.ctx run function #bs.fsm:new { \ -# name: "invalid_fsm_4", \ -# fsm: { \ -# initial: "idle", \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit", \ -# transitions: [ \ -# { \ -# name: "start", \ -# condition: "manual", \ -# to: "nonexistent" \ -# } \ -# ] \ -# } \ -# ] \ -# } \ -# } -# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when transition points to non-existent state" +# Test 6: Create FSM with transition to non-existent state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_4", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "start", \ + condition: "manual", \ + to: "nonexistent" \ + }, \ + { \ + name: "start", \ + condition: "manual", \ + to: "active" \ + } \ + ] \ + }, \ + { \ + name: "active", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run say "Test 6: Failed to return an error when transition points to non-existent state" -# ## === VALID FSM WITH COMPLEX TRANSITIONS === +## === VALID FSM WITH COMPLEX TRANSITIONS === -# # Test 7: Create a valid FSM with different transition types -# function #bs.fsm:new { \ -# name: "complex_fsm", \ -# fsm: { \ -# initial: "start", \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "start", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit", \ -# transitions: [ \ -# { \ -# name: "manual_transition", \ -# condition: "manual", \ -# to: "waiting" \ -# }, \ -# { \ -# name: "predicate_transition", \ -# condition: { type: "predicate", wait: "bs.fsm:test/condition" }, \ -# to: "processing" \ -# }, \ -# { \ -# name: "function_transition", \ -# condition: { type: "function", wait: "bs.fsm:test/function" }, \ -# to: "processing" \ -# }, \ -# { \ -# name: "hook_transition", \ -# condition: { type: "hook", wait: "bs.fsm:test/hook" }, \ -# to: "processing" \ -# }, \ -# { \ -# name: "delay_transition", \ -# condition: { type: "delay", wait: "20t" }, \ -# to: "processing" \ -# } \ -# ] \ -# }, \ -# { \ -# name: "waiting", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit" \ -# }, \ -# { \ -# name: "processing", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit", \ -# final: true \ -# } \ -# ] \ -# } \ -# } -# execute unless data storage bs:data fsm.fsm.complex_fsm run say "Failed to create a complex FSM with various transition types" +# Test 7: Create a valid FSM with different transition types +function #bs.fsm:new { \ + name: "complex_fsm", \ + fsm: { \ + initial: "start", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "start", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "manual_transition", \ + condition: "manual", \ + to: "waiting" \ + }, \ + { \ + name: "predicate_transition", \ + condition: { type: "predicate", wait: "bs.fsm:test/condition" }, \ + to: "processing" \ + }, \ + { \ + name: "function_transition", \ + condition: { type: "function", wait: "bs.fsm:test/function" }, \ + to: "processing" \ + }, \ + { \ + name: "hook_transition", \ + condition: { type: "hook", wait: "bs.fsm:test/hook" }, \ + to: "processing" \ + }, \ + { \ + name: "delay_transition", \ + condition: { type: "delay", wait: "20t" }, \ + to: "processing" \ + } \ + ] \ + }, \ + { \ + name: "waiting", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "manual_transition", \ + condition: "manual", \ + to: "processing" \ + } \ + ] \ + }, \ + { \ + name: "processing", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ + } \ + ] \ + } \ +} +execute unless data storage bs:data fsm.fsm.complex_fsm run say "Test 7: Failed to create a complex FSM with various transition types" -# ## === VALID FSM WITH MINIMAL CONFIGURATION === +## === VALID FSM WITH MINIMAL CONFIGURATION === -# # Test 8: Create a minimal valid FSM -# function #bs.fsm:new { \ -# name: "minimal_fsm", \ -# fsm: { \ -# initial: "state1", \ -# states: [ \ -# { \ -# name: "state1", \ -# final: true \ -# } \ -# ] \ -# } \ -# } -# execute unless data storage bs:data fsm.fsm.minimal_fsm run say "Failed to create a minimal FSM" +# Test 8: Create a minimal valid FSM +function #bs.fsm:new { \ + name: "minimal_fsm", \ + fsm: { \ + initial: "state1", \ + states: [ \ + { \ + name: "state1", \ + final: true \ + } \ + ] \ + } \ +} +execute unless data storage bs:data fsm.fsm.minimal_fsm run say "Test 8: Failed to create a minimal FSM" -# ## === INVALID FSM - UNREACHABLE FINAL STATE === +## === INVALID FSM - UNREACHABLE FINAL STATE === -# # Test 9: Create FSM with unreachable final state -# execute store success score #s bs.ctx run function #bs.fsm:new { \ -# name: "invalid_fsm_5", \ -# fsm: { \ -# initial: "idle", \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit", \ -# final: false \ -# }, \ -# { \ -# name: "final_state", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit", \ -# final: true \ -# } \ -# ] \ -# } \ -# } -# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has unreachable final state" +# Test 9: Create FSM with unreachable final state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_5", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: false \ + }, \ + { \ + name: "final_state", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run say "Test 9: Failed to return an error when FSM has unreachable final state" -# ## === INVALID FSM - NO FINAL STATE === +## === INVALID FSM - NO FINAL STATE === -# # Test 10: Create FSM with no final state -# execute store success score #s bs.ctx run function #bs.fsm:new { \ -# name: "invalid_fsm_6", \ -# fsm: { \ -# initial: "idle", \ -# on_cancel: "bs.fsm:test/cancel", \ -# states: [ \ -# { \ -# name: "idle", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit", \ -# final: false \ -# }, \ -# { \ -# name: "active", \ -# on_tick: "bs.fsm:test/tick", \ -# on_enter: "bs.fsm:test/enter", \ -# on_exit: "bs.fsm:test/exit", \ -# final: false \ -# } \ -# ] \ -# } \ -# } -# execute unless score #s bs.ctx matches 0 run say "Failed to return an error when FSM has no final state" +# Test 10: Create FSM with no final state +execute store success score #s bs.ctx run function #bs.fsm:new { \ + name: "invalid_fsm_6", \ + fsm: { \ + initial: "idle", \ + on_cancel: "bs.fsm:test/cancel", \ + states: [ \ + { \ + name: "idle", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: false \ + }, \ + { \ + name: "active", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: false \ + } \ + ] \ + } \ +} +execute unless score #s bs.ctx matches 0 run say "Test 10: Failed to return an error when FSM has no final state" -# ## === CLEANUP === +## === CLEANUP === -# # Clean up test data -# data remove storage bs:data fsm.fsm +# Clean up test data +data remove storage bs:data fsm.fsm diff --git a/modules/bs.fsm/data/bs.fsm/function/start.mcfunction b/modules/bs.fsm/data/bs.fsm/function/start.mcfunction index f4932fb33..dcbb6470e 100644 --- a/modules/bs.fsm/data/bs.fsm/function/start.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/start.mcfunction @@ -2,3 +2,29 @@ # Macro: fsm_name: string # Macro: instance_name: string # Macro: bind: "global" | "local" + +$data modify storage bs:ctx _ set value { bind: $(bind), fsm_name: $(fsm_name), instance_name: $(instance_name) } + +data modify storage bs:ctx _.test set value "global" +execute store success score #s bs.ctx run data modify storage bs:ctx _.test set from storage bs:ctx _.bind + +# We check if the instance already exists +$execute if score #s bs.ctx matches 0 store success score #e bs.ctx if data storage bs:data fsm.running_instances.'$(instance_name)' +execute if score #e bs.ctx matches 1 run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:start", \ + tag: "start", \ + message: [{text: "An instance with the name '"}, {nbt: "_.instance_name", storage: "bs:ctx"}, {text: "' already exists in the global context."}] \ +} +execute if score #e bs.ctx matches 1 run return fail + +$execute store success score #s bs.ctx run data modify storage bs:data fsm.running_instances.'$(instance_name)' set from storage bs:data fsm.fsm.'$(fsm_name)' +execute if score #s bs.ctx matches 0 run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:start", \ + tag: "start", \ + message: [{text: "The FSM '"}, {nbt: "_.fsm_name", storage: "bs:ctx"}, {text: "' does not exist."}] \ +} +execute if score #s bs.ctx matches 0 run return fail + +execute if data storage bs:data fsm.running_instances.'$(instance_name)' diff --git a/modules/bs.fsm/data/bs.fsm/test/new.mcfunction b/modules/bs.fsm/data/bs.fsm/test/new.mcfunction index 3e6eca795..4c8dd3480 100644 --- a/modules/bs.fsm/data/bs.fsm/test/new.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/test/new.mcfunction @@ -156,8 +156,20 @@ execute store success score #s bs.ctx run function #bs.fsm:new { \ name: "start", \ condition: "manual", \ to: "nonexistent" \ + }, \ + { \ + name: "start", \ + condition: "manual", \ + to: "active" \ } \ ] \ + }, \ + { \ + name: "active", \ + on_tick: "bs.fsm:test/tick", \ + on_enter: "bs.fsm:test/enter", \ + on_exit: "bs.fsm:test/exit", \ + final: true \ } \ ] \ } \ @@ -210,7 +222,14 @@ function #bs.fsm:new { \ name: "waiting", \ on_tick: "bs.fsm:test/tick", \ on_enter: "bs.fsm:test/enter", \ - on_exit: "bs.fsm:test/exit" \ + on_exit: "bs.fsm:test/exit", \ + transitions: [ \ + { \ + name: "manual_transition", \ + condition: "manual", \ + to: "processing" \ + } \ + ] \ }, \ { \ name: "processing", \ From 5cac2a69b307c438a27a0dc5d71f983b03e8c4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Sun, 20 Jul 2025 11:33:06 +0200 Subject: [PATCH 08/14] Add start feat --- docs/modules/fsm.md | 52 +++++++++++-- .../data/bs.fsm/function/__load__.mcfunction | 5 +- .../well_formedness_transition.mcfunction | 13 ++-- .../data/bs.fsm/function/new.mcfunction | 8 +- .../function/run/check_entity.mcfunction | 4 + .../run/enter_state_global.mcfunction | 29 ++++++++ .../function/run/enter_state_local.mcfunction | 31 ++++++++ .../run/leave_state_global.mcfunction | 0 .../run/run_command_global.mcfunction | 1 + .../function/run/run_command_local.mcfunction | 1 + .../data/bs.fsm/function/run/tick.mcfunction | 5 ++ .../bs.fsm/function/run/tick_rec.mcfunction | 30 ++++++++ .../data/bs.fsm/function/start.mcfunction | 10 +-- .../data/bs.fsm/function/start_as.mcfunction | 37 ++++++++++ .../data/bs.fsm/function/test.mcfunction | 74 +++++++++++++++++++ .../data/bs.fsm/tags/function/start.json | 20 +++++ .../data/bs.fsm/tags/function/start_as.json | 20 +++++ 17 files changed, 315 insertions(+), 25 deletions(-) create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/check_entity.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/leave_state_global.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/tick.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/start_as.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/test.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/tags/function/start.json create mode 100644 modules/bs.fsm/data/bs.fsm/tags/function/start_as.json diff --git a/docs/modules/fsm.md b/docs/modules/fsm.md index 33f9f8b0b..2fc9a0592 100644 --- a/docs/modules/fsm.md +++ b/docs/modules/fsm.md @@ -142,9 +142,12 @@ function #bs.fsm:new { \ ### Start +```{tab-set} +```{tab-item} Global Instance + ```{function} #bs.fsm:start -Start a new instance of a Finite State Machine. +Start a new global instance of a Finite State Machine. :Inputs: **Function macro**: @@ -152,28 +155,63 @@ Start a new instance of a Finite State Machine. - {nbt}`compound` Arguments - {nbt}`string` **fsm_name**: Name of the FSM to instantiate (must exist). - {nbt}`string` **instance_name**: Unique identifier for this FSM instance. - - {nbt}`string` **bind**: Binding type for the instance. - - **"global"**: Instance is bound globally and accessible from anywhere. - - **"local"**: Instance is bound to the current execution context. ::: :Outputs: **Return**: Success (1) if instance was started successfully, failure (0) otherwise. - **State**: The FSM instance is created and begins execution in its initial state. + **State**: The FSM instance is created globally and begins execution in its initial state. ``` *Example: Start a door FSM instance:* ```mcfunction # Start a door FSM instance -function #bs.fsm:start { fsm_name: "door_fsm", instance_name: "main_door", bind: "global" } +function #bs.fsm:start { fsm_name: "door_fsm", instance_name: "main_door" } -# The door FSM is now running and will execute its initial state +# The door FSM is now running globally and will execute its initial state ``` > **Credits**: theogiraudet +``` + +```{tab-item} Local Instance + +```{function} #bs.fsm:start_as + +Start new local instances of a Finite State Machine bound to the executing entities. + +:Inputs: + **Execution `as `**: Entities to bind. The entities must not be players. + + **Function macro**: + :::{treeview} + - {nbt}`compound` Arguments + - {nbt}`string` **fsm_name**: Name of the FSM to instantiate (must exist). + - {nbt}`string` **instance_name**: Unique identifier for this FSM instance. + ::: + +:Outputs: + **Return**: Success (1) if instance was started successfully, failure (0) otherwise. + + **State**: The FSM instances are created locally for the executing entities and begins execution in their initial state. +``` + +*Example: Start a door FSM instance for an entity:* + +```mcfunction +# Start a door FSM instance bound to the executing entity +execute as @n[type=zombie] run function #bs.fsm:start_as { fsm_name: "door_fsm", instance_name: "entity_door" } + +# The door FSM is now running locally for this zombie and will execute its initial state +``` + +> **Credits**: theogiraudet + +``` +``` + --- ### Cancel diff --git a/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction index f5419eb51..11177606f 100644 --- a/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction @@ -1,4 +1,7 @@ forceload add -30000000 1600 execute unless entity B5-0-0-0-1 run summon minecraft:marker -30000000 0 1600 {UUID:[I;181,0,0,1],Tags:["bs.entity","bs.persistent","smithed.entity","smithed.strict"]} -execute unless data storage bs:data fsm run data modify storage bs:data fsm set value { fsm: {}, running_instances: {} } +execute unless entity B5-0-0-0-2 run summon minecraft:text_display -30000000 0 1600 {UUID:[I;181,0,0,2],Tags:["bs.entity","bs.persistent","smithed.entity","smithed.strict"],view_range:0f,alignment:"center"} +execute unless data storage bs:data fsm run data modify storage bs:data fsm merge value { fsm: {}, running_instances: {}, listened_transitions: [], ticks: [] } + +execute if data storage bs:data fsm.ticks[0] run schedule function bs.fsm:run/tick 1t diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction index 57b337d47..d80085119 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction @@ -45,14 +45,13 @@ execute store success score #s bs.ctx run data modify storage bs:ctx _.condition execute if score #s bs.ctx matches 0 run return 1 # If the condition is not "manual", we need to check if the condition is an object -execute store success score #s bs.ctx run data modify storage bs:ctx _.condition merge value {test: true} -execute if score #s bs.ctx matches 0 run function #bs.log:error { \ +execute unless data storage bs:ctx _.states[0].transitions[0].condition.type run function #bs.log:error { \ namespace: "bs.fsm", \ path: "#bs.fsm:new", \ tag: "new", \ message: [{text: "A transition of '"}, {nbt: "_.states[0].name", storage: "bs:ctx"}, {text: "' has an invalid condition."}] \ } -execute if score #s bs.ctx matches 0 run return fail +execute unless data storage bs:ctx _.states[0].transitions[0].condition.type run return fail # Now, we need to check the validity of the condition object, notably the wait execute unless data storage bs:ctx _.states[0].transitions[0].condition.wait run function #bs.log:error { \ @@ -67,10 +66,10 @@ execute unless data storage bs:ctx _.states[0].transitions[0].condition.wait run data modify storage bs:ctx _.condition set value [] data modify storage bs:ctx _.condition append from storage bs:ctx _.states[0].transitions[0].condition -execute store success score #s bs.ctx unless data storage bs:ctx _.condition[{type: "predicate"}] \ -unless data storage bs:ctx _.condition[{type: "function"}] \ -unless data storage bs:ctx _.condition[{type: "hook"}] \ -unless data storage bs:ctx _.condition[{type: "delay"}] +execute store success score #s bs.ctx unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "predicate"}] \ +unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "function"}] \ +unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "hook"}] \ +unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "delay"}] execute if score #s bs.ctx matches 0 run function #bs.log:error { \ namespace: "bs.fsm", \ diff --git a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction index 7b314373e..052029ba9 100644 --- a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction @@ -2,13 +2,13 @@ # Macro: name: string # Macro: fsm: { # initial: state -# on_cancel?: function +# on_cancel?: command # states: [ # { # name: string -# on_tick?: function -# on_exit?: function -# on_enter?: function +# on_tick?: command +# on_exit?: command +# on_enter?: command # final?: boolean # transitions?: [ # { diff --git a/modules/bs.fsm/data/bs.fsm/function/run/check_entity.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/check_entity.mcfunction new file mode 100644 index 000000000..d86d23335 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/check_entity.mcfunction @@ -0,0 +1,4 @@ +# Input: +# - Macro context: + +$return run execute if entity $(context) diff --git a/modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction new file mode 100644 index 000000000..5bc477ce9 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction @@ -0,0 +1,29 @@ +# Input: +# - Macro instance_name: string +# - Macro state_name: string - new current state name + +# We set the new state as current state +$data modify storage bs:data fsm.running_instances.'$(instance_name)'.states[{name: "$(state_name)"}].current set value true + +$data modify storage bs:ctx _.state set from storage bs:data fsm.running_instances.'$(instance_name)'.states[{name: "$(state_name)"}] + +# We prepare the transitions to be listened +data modify storage bs:ctx _.tmp set from storage bs:ctx _.state.transitions +data modify storage bs:ctx _.tmp[].source set from storage bs:ctx _.state.name +data modify storage bs:ctx _.tmp[].context set value "global" +$execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp.instance_name set value "$(instance_name)" + +# We add the transitions to the listened transitions list +data modify storage bs:data fsm.listened_transitions append from storage bs:ctx _.tmp[] + +# We execute the on_enter command +execute if data storage bs:ctx _.state.on_enter run data modify storage bs:ctx _.command set from storage bs:ctx _.state.on_enter +execute if data storage bs:ctx _.state.on_enter run function bs.fsm:run/run_command_global with storage bs:ctx _ + +# We register the on_tick command +execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp set value {} +execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp.command set from storage bs:ctx _.state.on_tick +$execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp.instance_name set value "$(instance_name)" +execute if data storage bs:ctx _.state.on_tick run data modify storage bs:data fsm.ticks append from storage bs:ctx _.tmp +# If this is the only command on the ticks list, we start the tick loop +execute unless data storage bs:data fsm.ticks[1] run schedule function bs.fsm:run/tick 1t diff --git a/modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction new file mode 100644 index 000000000..41bf42f30 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction @@ -0,0 +1,31 @@ +# Input: +# - Macro instance_name: string +# - Macro state_name: string - new current state name +# - Macro context: + +# We set the new state as current state +$data modify entity $(context) data.bs:fsm.running_instances.'$(instance_name)'.states[{name: "$(state_name)"}].current set value true + +$data modify storage bs:ctx _.state set from entity $(context) data.bs:fsm.running_instances.'$(instance_name)'.states[{name: "$(state_name)"}] +$data modify storage bs:ctx _.context set value "$(context)" + +# We prepare the transitions to be listened +data modify storage bs:ctx _.tmp set from storage bs:ctx _.state.transitions +data modify storage bs:ctx _.tmp[].source set from storage bs:ctx _.state.name +data modify storage bs:ctx _.tmp[].context set from storage bs:ctx _.context + +# We add the transitions to the listened transitions list +data modify storage bs:data fsm.listened_transitions append from storage bs:ctx _.tmp[] + +# We execute the on_enter command +execute if data storage bs:ctx _.state.on_enter run data modify storage bs:ctx _.command set from storage bs:ctx _.state.on_enter +execute if data storage bs:ctx _.state.on_enter run function bs.fsm:run/run_command_local with storage bs:ctx _ + +# We register the on_tick command, for that we create an object with the context (i.e., the UUID of the entity) and the command +execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp set value {} +execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp.context set from storage bs:ctx _.context +execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp.command set from storage bs:ctx _.state.on_tick +$execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp.instance_name set value "$(instance_name)" +execute if data storage bs:ctx _.state.on_tick run data modify storage bs:data fsm.ticks append from storage bs:ctx _.tmp +# If this is the only command on the ticks list, we start the tick loop +execute if data storage bs:data fsm.ticks[0] unless data storage bs:data fsm.ticks[1] run schedule function bs.fsm:run/tick 1t diff --git a/modules/bs.fsm/data/bs.fsm/function/run/leave_state_global.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/leave_state_global.mcfunction new file mode 100644 index 000000000..e69de29bb diff --git a/modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction new file mode 100644 index 000000000..e3fa08ab0 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction @@ -0,0 +1 @@ +$$(command) diff --git a/modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction new file mode 100644 index 000000000..a87d7c8fb --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction @@ -0,0 +1 @@ +$execute as $(context) at $(context) run $(command) diff --git a/modules/bs.fsm/data/bs.fsm/function/run/tick.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/tick.mcfunction new file mode 100644 index 000000000..59eb61329 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/tick.mcfunction @@ -0,0 +1,5 @@ +execute store result score #c bs.ctx run data get storage bs:data fsm.ticks +function bs.fsm:run/tick_rec + +# If we still have functions to run next tick, we schedule again +execute if data storage bs:data fsm.ticks[0] run schedule function bs.fsm:run/tick 1t diff --git a/modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction new file mode 100644 index 000000000..a1fadc885 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction @@ -0,0 +1,30 @@ +# Input: +# - Storage bs:data fsm.ticks: {context: , command: } | { command: } + +# Terminal cases: +# The list is empty +execute unless data storage bs:data fsm.ticks[0] run return 1 +# We already ran all the commands +execute if score #c bs.ctx matches ..0 run return 1 + +# We reduce the counter +scoreboard players remove #c bs.ctx 1 + +# If the command is global, we run it +execute unless data storage bs:data fsm.ticks[0].context run function bs.fsm:run/run_command_global with storage bs:data fsm.ticks[0] + +scoreboard players set #s bs.ctx -1 +# Else, we first check if the entity exists, otherwise we remove the command from the tick list +execute if data storage bs:data fsm.ticks[0].context store success score #s bs.ctx run function bs.fsm:run/check_entity with storage bs:data fsm.ticks[0] +execute if score #s bs.ctx matches 0 run data remove storage bs:data fsm.ticks[0] + +# We run the command only if the entity exists +execute if score #s bs.ctx matches 1 run function bs.fsm:run/run_command_local with storage bs:data fsm.ticks[0] + +# If the entity exists, or if we are on a global command, we shift the tick list +# We do not shift the tick list if the entity does not exist since we already removed the command +execute unless score #s bs.ctx matches 0 run data modify storage bs:data fsm.ticks append from storage bs:data fsm.ticks[0] +execute unless score #s bs.ctx matches 0 run data remove storage bs:data fsm.ticks[0] + +# Recursion to continue the commands execution +function bs.fsm:run/tick_rec diff --git a/modules/bs.fsm/data/bs.fsm/function/start.mcfunction b/modules/bs.fsm/data/bs.fsm/function/start.mcfunction index dcbb6470e..5fe3f3788 100644 --- a/modules/bs.fsm/data/bs.fsm/function/start.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/start.mcfunction @@ -1,12 +1,8 @@ # Input: # Macro: fsm_name: string # Macro: instance_name: string -# Macro: bind: "global" | "local" -$data modify storage bs:ctx _ set value { bind: $(bind), fsm_name: $(fsm_name), instance_name: $(instance_name) } - -data modify storage bs:ctx _.test set value "global" -execute store success score #s bs.ctx run data modify storage bs:ctx _.test set from storage bs:ctx _.bind +$data modify storage bs:ctx _ set value { fsm_name: $(fsm_name), instance_name: $(instance_name) } # We check if the instance already exists $execute if score #s bs.ctx matches 0 store success score #e bs.ctx if data storage bs:data fsm.running_instances.'$(instance_name)' @@ -27,4 +23,6 @@ execute if score #s bs.ctx matches 0 run function #bs.log:error { \ } execute if score #s bs.ctx matches 0 run return fail -execute if data storage bs:data fsm.running_instances.'$(instance_name)' +# If the FSM is global, we enter the initial state +$data modify storage bs:ctx _.state_name set from storage bs:data fsm.fsm.'$(fsm_name)'.initial +function bs.fsm:run/enter_state_global with storage bs:ctx _ diff --git a/modules/bs.fsm/data/bs.fsm/function/start_as.mcfunction b/modules/bs.fsm/data/bs.fsm/function/start_as.mcfunction new file mode 100644 index 000000000..f1689c982 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/start_as.mcfunction @@ -0,0 +1,37 @@ +# Input: +# Macro: fsm_name: string +# Macro: instance_name: string + +$data modify storage bs:ctx _ set value { fsm_name: $(fsm_name), instance_name: $(instance_name) } + +# We get the String UUID of the entity +tag @s add bs.fsm.entity +data modify entity B5-0-0-0-2 text set value { selector: "@n[tag=bs.fsm.entity]" } +data modify storage bs:ctx _.context set from entity B5-0-0-0-2 text.insertion + +# We check if the instance already exists +$execute store success score #s bs.ctx if data entity @n[tag=bs.fsm.entity] data.bs:fsm.running_instances.'$(instance_name)' +execute if score #s bs.ctx matches 1 run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:start_as", \ + tag: "start_as", \ + message: [{text: "An instance with the name '"}, {nbt: "_.instance_name", storage: "bs:ctx"}, {text: "' already exists for entity '"}, {nbt: "_.context", storage: "bs:ctx"}, {text: "'."}] \ +} +execute if score #s bs.ctx matches 1 run tag @s remove bs.fsm.entity +execute if score #s bs.ctx matches 1 run return fail + +# We create the instance of the FSM for the entity +$execute store success score #s bs.ctx run data modify entity @n[tag=bs.fsm.entity] data.bs:fsm.running_instances.'$(instance_name)' set from storage bs:data fsm.fsm.'$(fsm_name)' +# If the instance was not created, we log an error since the FSM does not exist +execute if score #s bs.ctx matches 0 run function #bs.log:error { \ + namespace: "bs.fsm", \ + path: "#bs.fsm:start_as", \ + tag: "start_as", \ + message: [{text: "The FSM '"}, {nbt: "_.fsm_name", storage: "bs:ctx"}, {text: "' does not exist."}] \ +} +execute if score #s bs.ctx matches 0 run tag @s remove bs.fsm.entity +execute if score #s bs.ctx matches 0 run return fail + +# We enter the initial state +$data modify storage bs:ctx _.state_name set from storage bs:data fsm.fsm.'$(fsm_name)'.initial +function bs.fsm:run/enter_state_local with storage bs:ctx _ diff --git a/modules/bs.fsm/data/bs.fsm/function/test.mcfunction b/modules/bs.fsm/data/bs.fsm/function/test.mcfunction new file mode 100644 index 000000000..4598b68b2 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/test.mcfunction @@ -0,0 +1,74 @@ +execute unless entity B5-0-0-0-9 run summon marker ~ ~ ~ {Tags:["bs.entity", "bs.fsm.test"], UUID:[I;181,0,0,9]} + +# Create a traffic light FSM +function bs.fsm:new { \ + name: "traffic_light", \ + fsm: { \ + initial: "red", \ + states: [ \ + { \ + name: "red", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"red\"},{\"text\":\" Red light - Stop!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"red_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + transitions: [ \ + { \ + name: "to_green", \ + condition: { type: "delay", wait: "20s" }, \ + to: "green" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "green", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"light_green\"},{\"text\":\" Green light - Go!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"green_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + transitions: [ \ + { \ + name: "to_orange", \ + condition: { type: "delay", wait: "30s" }, \ + to: "orange" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "orange", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"gold\"},{\"text\":\" Orange light - Prepare to stop!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"yellow_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + transitions: [ \ + { \ + name: "to_red", \ + condition: { type: "delay", wait: "5s" }, \ + to: "red" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "off", \ + final: true, \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"black\"},{\"text\":\" Off light - Be careful!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"black_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + } \ + ] \ + } \ +} + +# Start the traffic light FSM +execute as B5-0-0-0-9 run function bs.fsm:start_as { \ + fsm_name: "traffic_light", \ + instance_name: "main_traffic_light" \ +} diff --git a/modules/bs.fsm/data/bs.fsm/tags/function/start.json b/modules/bs.fsm/data/bs.fsm/tags/function/start.json new file mode 100644 index 000000000..e4b9f6b12 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/tags/function/start.json @@ -0,0 +1,20 @@ +{ + "__bookshelf__": { + "feature": true, + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#start", + "authors": [ + "theogiraudet" + ], + "created": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + }, + "updated": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + } + }, + "values": [ + "bs.fsm:start" + ] +} diff --git a/modules/bs.fsm/data/bs.fsm/tags/function/start_as.json b/modules/bs.fsm/data/bs.fsm/tags/function/start_as.json new file mode 100644 index 000000000..62c59ada4 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/tags/function/start_as.json @@ -0,0 +1,20 @@ +{ + "__bookshelf__": { + "feature": true, + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#start-as", + "authors": [ + "theogiraudet" + ], + "created": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + }, + "updated": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + } + }, + "values": [ + "bs.fsm:start_as" + ] +} From f5b1ec481831c34e30e0f82156381d781ebd4068 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 09:33:40 +0000 Subject: [PATCH 09/14] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20chore:=20update?= =?UTF-8?q?=20generated=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/manifest.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/data/manifest.json b/data/manifest.json index 0cc20256b..c808af626 100644 --- a/data/manifest.json +++ b/data/manifest.json @@ -779,6 +779,36 @@ "date": "2025/07/16", "minecraft_version": "1.21.7" } + }, + { + "id": "#bs.fsm:start", + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#start", + "authors": [ + "theogiraudet" + ], + "created": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + }, + "updated": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + } + }, + { + "id": "#bs.fsm:start_as", + "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#start-as", + "authors": [ + "theogiraudet" + ], + "created": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + }, + "updated": { + "date": "2025/07/20", + "minecraft_version": "1.21.8" + } } ] }, From 7d1fb9089f2d43ccf303ea203d26feb6c731ad10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Sun, 20 Jul 2025 14:22:08 +0200 Subject: [PATCH 10/14] Add fsm evaluation --- docs/modules/fsm.md | 10 +++--- .../data/bs.fsm/function/__load__.mcfunction | 4 +-- .../well_formedness_transition.mcfunction | 2 +- .../data/bs.fsm/function/new.mcfunction | 2 +- .../bs.fsm/function/new_vanilla.mcfunction | 2 +- .../run/enter_state_global.mcfunction | 11 +++++- .../function/run/enter_state_local.mcfunction | 9 +++++ .../evaluate_command_transition.mcfunction | 18 ++++++++++ .../run/evaluate_delay_transition.mcfunction | 8 +++++ .../run/evaluate_global_predicate.mcfunction | 1 + .../run/evaluate_local_predicate.mcfunction | 1 + .../evaluate_predicate_transition.mcfunction | 18 ++++++++++ .../run/evaluate_transitions.mcfunction | 14 ++++++++ .../run/evaluate_transitions_rec.mcfunction | 36 +++++++++++++++++++ .../run/leave_state_global.mcfunction | 0 .../run/run_command_global.mcfunction | 2 +- .../function/run/run_command_local.mcfunction | 2 +- .../function/run/switch_state.mcfunction | 31 ++++++++++++++++ .../bs.fsm/function/run/tick_rec.mcfunction | 4 +-- .../data/bs.fsm/function/test.mcfunction | 12 ++++--- modules/bs.fsm/data/bs.fsm/function/todo.md | 3 ++ .../bs.fsm/data/bs.fsm/test/new.mcfunction | 2 +- 22 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/evaluate_delay_transition.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/evaluate_global_predicate.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/evaluate_local_predicate.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction delete mode 100644 modules/bs.fsm/data/bs.fsm/function/run/leave_state_global.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/run/switch_state.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/todo.md diff --git a/docs/modules/fsm.md b/docs/modules/fsm.md index 2fc9a0592..0bd9411e6 100644 --- a/docs/modules/fsm.md +++ b/docs/modules/fsm.md @@ -50,16 +50,16 @@ Create a new Finite State Machine (FSM) with the specified configuration. - **"manual"**: Manual transition triggered by external call. - {nbt}`compound` **predicate**: Predicate-based transition. - {nbt}`string` **type**: Must be "predicate". - - {nbt}`string` **wait**: Predicate function to evaluate. - - {nbt}`compound` **function**: Function-based transition. - - {nbt}`string` **type**: Must be "function". - - {nbt}`string` **wait**: Function to call for evaluation. + - {nbt}`string` **wait**: Predicate to check to trigger the transition. + - {nbt}`compound` **command**: Command-based transition. + - {nbt}`string` **type**: Must be "command". + - {nbt}`string` **wait**: Command to check to trigger the transition. - {nbt}`compound` **hook**: Hook-based transition. - {nbt}`string` **type**: Must be "hook". - {nbt}`string` **wait**: Hook function to evaluate. - {nbt}`compound` **delay**: Time-based transition. - {nbt}`string` **type**: Must be "delay". - - {nbt}`string` **wait**: Time delay (e.g., "20t" for 1 second). + - {nbt}`string` **wait**: Time delay in ticks. - {nbt}`string` **to**: Name of the target state (must exist in states array). ::: diff --git a/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction index 11177606f..7d244a5c1 100644 --- a/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/__load__.mcfunction @@ -2,6 +2,6 @@ forceload add -30000000 1600 execute unless entity B5-0-0-0-1 run summon minecraft:marker -30000000 0 1600 {UUID:[I;181,0,0,1],Tags:["bs.entity","bs.persistent","smithed.entity","smithed.strict"]} execute unless entity B5-0-0-0-2 run summon minecraft:text_display -30000000 0 1600 {UUID:[I;181,0,0,2],Tags:["bs.entity","bs.persistent","smithed.entity","smithed.strict"],view_range:0f,alignment:"center"} -execute unless data storage bs:data fsm run data modify storage bs:data fsm merge value { fsm: {}, running_instances: {}, listened_transitions: [], ticks: [] } +execute unless data storage bs:data fsm run data modify storage bs:data fsm set value { fsm: {}, running_instances: {}, listened_transitions: [], ticks: [] } -execute if data storage bs:data fsm.ticks[0] run schedule function bs.fsm:run/tick 1t +# execute if data storage bs:data fsm.ticks[0] run schedule function bs.fsm:run/tick 1t diff --git a/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction index d80085119..2abae907c 100644 --- a/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/check/internal/well_formedness_transition.mcfunction @@ -67,7 +67,7 @@ data modify storage bs:ctx _.condition set value [] data modify storage bs:ctx _.condition append from storage bs:ctx _.states[0].transitions[0].condition execute store success score #s bs.ctx unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "predicate"}] \ -unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "function"}] \ +unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "command"}] \ unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "hook"}] \ unless data storage bs:ctx _.states[0].transitions[0].condition[{type: "delay"}] diff --git a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction index 052029ba9..2353f4469 100644 --- a/modules/bs.fsm/data/bs.fsm/function/new.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/new.mcfunction @@ -13,7 +13,7 @@ # transitions?: [ # { # name?: string -# condition: 'manual' | { type: 'predicate', wait: string } | { type: 'function', wait: string } | { type: 'hook', wait: string } | { type: 'delay', wait: string } +# condition: 'manual' | { type: 'predicate', wait: string } | { type: 'command', wait: string } | { type: 'hook', wait: string } | { type: 'delay', wait: string } # to: state # } # ] diff --git a/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction b/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction index 064e2037d..09fa95ad7 100644 --- a/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/new_vanilla.mcfunction @@ -203,7 +203,7 @@ function #bs.fsm:new { \ }, \ { \ name: "function_transition", \ - condition: { type: "function", wait: "bs.fsm:test/function" }, \ + condition: { type: "command", wait: "bs.fsm:test/function" }, \ to: "processing" \ }, \ { \ diff --git a/modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction index 5bc477ce9..68c8f1398 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_global.mcfunction @@ -9,13 +9,22 @@ $data modify storage bs:ctx _.state set from storage bs:data fsm.running_instanc # We prepare the transitions to be listened data modify storage bs:ctx _.tmp set from storage bs:ctx _.state.transitions +# We remove the manual transitions since we do not need to listen to them +data remove storage bs:ctx _.tmp[{condition: "manual"}] data modify storage bs:ctx _.tmp[].source set from storage bs:ctx _.state.name data modify storage bs:ctx _.tmp[].context set value "global" -$execute if data storage bs:ctx _.state.on_tick run data modify storage bs:ctx _.tmp.instance_name set value "$(instance_name)" +data modify storage bs:ctx _.tmp[].global set value true +$data modify storage bs:ctx _.tmp[].instance_name set value "$(instance_name)" + +# We check the listened_transitions list size +execute store result score #s bs.ctx run data get storage bs:data fsm.listened_transitions # We add the transitions to the listened transitions list data modify storage bs:data fsm.listened_transitions append from storage bs:ctx _.tmp[] +# If before, the listened_transitions list was empty, we start the listened transitions loop +execute if score #s bs.ctx matches ..0 run schedule function bs.fsm:run/evaluate_transitions 1t + # We execute the on_enter command execute if data storage bs:ctx _.state.on_enter run data modify storage bs:ctx _.command set from storage bs:ctx _.state.on_enter execute if data storage bs:ctx _.state.on_enter run function bs.fsm:run/run_command_global with storage bs:ctx _ diff --git a/modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction index 41bf42f30..608ec7ab4 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/enter_state_local.mcfunction @@ -11,12 +11,21 @@ $data modify storage bs:ctx _.context set value "$(context)" # We prepare the transitions to be listened data modify storage bs:ctx _.tmp set from storage bs:ctx _.state.transitions +# We remove the manual transitions since we do not need to listen to them +data remove storage bs:ctx _.tmp[{condition: "manual"}] data modify storage bs:ctx _.tmp[].source set from storage bs:ctx _.state.name data modify storage bs:ctx _.tmp[].context set from storage bs:ctx _.context +$data modify storage bs:ctx _.tmp[].instance_name set value "$(instance_name)" + +# We check the listened_transitions list size +execute store result score #s bs.ctx run data get storage bs:data fsm.listened_transitions # We add the transitions to the listened transitions list data modify storage bs:data fsm.listened_transitions append from storage bs:ctx _.tmp[] +# If before, the listened_transitions list was empty, we start the listened transitions loop +execute if score #s bs.ctx matches ..0 run schedule function bs.fsm:run/evaluate_transitions 1t + # We execute the on_enter command execute if data storage bs:ctx _.state.on_enter run data modify storage bs:ctx _.command set from storage bs:ctx _.state.on_enter execute if data storage bs:ctx _.state.on_enter run function bs.fsm:run/run_command_local with storage bs:ctx _ diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction new file mode 100644 index 000000000..132c03081 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction @@ -0,0 +1,18 @@ +# If this is a global function transition, we run the function and according to the result, we switch to the target state +execute if data storage bs:data fsm.listened_transitions[0].global \ + store success score #s bs.ctx \ + run function bs.fsm:run/run_command_global with storage bs:data fsm.listened_transitions[0].condition +execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] +execute if score #s bs.ctx matches 1 run return 1 + +# If this is a local function transition, we run the function and according to the result, we switch to the target state +data modify storage bs:ctx _.tmp set value {} +data modify storage bs:ctx _.tmp.command set from storage bs:data fsm.listened_transitions[0].condition.wait +data modify storage bs:ctx _.tmp.context set from storage bs:data fsm.listened_transitions[0].context +execute unless data storage bs:data fsm.listened_transitions[0].global \ + store success score #s bs.ctx \ + run function bs.fsm:run/run_command_local with storage bs:data fsm.listened_transitions[0] +execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:ctx _.tmp +execute if score #s bs.ctx matches 1 run return 1 + +return fail diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_delay_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_delay_transition.mcfunction new file mode 100644 index 000000000..2d9493e90 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_delay_transition.mcfunction @@ -0,0 +1,8 @@ +# We check if the delay has passed, if so, we switch to the target state. Otherwise, we decrease the delay +execute store result score #d bs.ctx run data get storage bs:data fsm.listened_transitions[0].condition.wait +execute if score #d bs.ctx matches ..0 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] +execute if score #d bs.ctx matches ..0 run return 1 +execute if score #d bs.ctx matches 1.. run scoreboard players remove #d bs.ctx 1 +execute store result storage bs:data fsm.listened_transitions[0].condition.wait int 1 run scoreboard players get #d bs.ctx + +return fail diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_global_predicate.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_global_predicate.mcfunction new file mode 100644 index 000000000..845fab051 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_global_predicate.mcfunction @@ -0,0 +1 @@ +$return run execute if predicate $(predicate) diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_local_predicate.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_local_predicate.mcfunction new file mode 100644 index 000000000..4e98979cc --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_local_predicate.mcfunction @@ -0,0 +1 @@ +$return run execute as $(context) at $(context) if predicate $(predicate) diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction new file mode 100644 index 000000000..3a5fc7a6d --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction @@ -0,0 +1,18 @@ +data modify storage bs:ctx _.transition set from storage bs:data fsm.listened_transitions[0] +data modify storage bs:ctx _.transition.predicate set from storage bs:ctx _.transition.condition.predicate + +# If this is a global predicate transition, we evaluate the predicate and according to the result, we switch to the target state +execute if data storage bs:data fsm.listened_transitions[0].global \ + store success score #s bs.ctx \ + run function bs.fsm:run/evaluate_global_predicate with storage bs:ctx _.transition +execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] +execute if score #s bs.ctx matches 1 run return 1 + +# If this is a local predicate transition, we evaluate the predicate and according to the result, we switch to the target state +execute unless data storage bs:data fsm.listened_transitions[0].global \ + store success score #s bs.ctx \ + run function bs.fsm:run/evaluate_local_predicate with storage bs:ctx _.transition +execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] +execute if score #s bs.ctx matches 1 run return 1 + +return fail diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions.mcfunction new file mode 100644 index 000000000..74e07132a --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions.mcfunction @@ -0,0 +1,14 @@ +# Input: +# - Storage bs:data fsm.listened_transitions: {source: , context: , command: , instance_name: , to: , condition: { type: "delay" | "predicate" | "function", wait: string }}[] + +# Unlike the tick function, we will not use a counter to know when to stop the recursion +# Indeed, in the tick function we don't unregister the tick commands, so we cannot loose the count +# In this function, we will remove the transitions that have been evaluated, as well as all the transitions for this instance +# As we don't know how many transitions will be removed, we prefer to use a flag on transitions to directly know from the current transition if we need to continue the recursion + +function bs.fsm:run/evaluate_transitions_rec +# We reset the checked flag for the next evaluation +data remove storage bs:data fsm.listened_transitions[].checked + +# If we still have transitions to evaluate, we schedule again +execute if data storage bs:data fsm.listened_transitions[0] run schedule function bs.fsm:run/evaluate_transitions 1t diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction new file mode 100644 index 000000000..96e54f00c --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction @@ -0,0 +1,36 @@ +# Input: +# - Storage bs:data fsm.listened_transitions: {source: , context: , command: , instance_name: , to: , condition: { type: "delay" | "predicate" | "function", wait: string }}[] + +# Terminal cases: +# The list is empty +execute unless data storage bs:data fsm.listened_transitions[0] run return 1 +# We already checked the current transition +execute if data storage bs:data fsm.listened_transitions[0].checked run return 1 + + +scoreboard players set #s bs.ctx -1 +# If this is a local transition, we check if the entity exists, otherwise we remove the transition from the list and we continue the recursion +execute unless data storage bs:data fsm.listened_transitions[0].global \ + store success score #s bs.ctx \ + run function bs.fsm:run/check_entity with storage bs:data fsm.listened_transitions[0] +execute if score #s bs.ctx matches 0 run data remove storage bs:data fsm.listened_transitions[0] +execute if score #s bs.ctx matches 0 run return run function bs.fsm:run/evaluate_transitions_rec + +# Delayed transition +execute store success score #s bs.ctx if data storage bs:data fsm.listened_transitions[0].condition{type: "delay"} run function bs.fsm:run/evaluate_delay_transition + +# Function transition +execute if score #s bs.ctx matches 0 store success score #s bs.ctx if data storage bs:data fsm.listened_transitions[0].condition{type: "command"} run function bs.fsm:run/evaluate_command_transition + +# Predicate transitions +execute if score #s bs.ctx matches 0 store success score #s bs.ctx if data storage bs:data fsm.listened_transitions[0].condition{type: "predicate"} run function bs.fsm:run/evaluate_predicate_transition + +# If we didn't evaluated the current transition, we set the transition as checked and we shift the list +execute if score #s bs.ctx matches 0 run data modify storage bs:data fsm.listened_transitions[0].checked set value true +execute if score #s bs.ctx matches 0 run data modify storage bs:data fsm.listened_transitions append from storage bs:data fsm.listened_transitions[0] +execute if score #s bs.ctx matches 0 run data remove storage bs:data fsm.listened_transitions[0] + +# If the current transition has been evaluated, we don't need to shift the list since the transition has been removed by the switch_state function + +# We continue the recursion +return run function bs.fsm:run/evaluate_transitions_rec diff --git a/modules/bs.fsm/data/bs.fsm/function/run/leave_state_global.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/leave_state_global.mcfunction deleted file mode 100644 index e69de29bb..000000000 diff --git a/modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction index e3fa08ab0..d056e421a 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/run_command_global.mcfunction @@ -1 +1 @@ -$$(command) +$return run $(command) diff --git a/modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction index a87d7c8fb..f0f218d07 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/run_command_local.mcfunction @@ -1 +1 @@ -$execute as $(context) at $(context) run $(command) +$return run execute as $(context) at $(context) run $(command) diff --git a/modules/bs.fsm/data/bs.fsm/function/run/switch_state.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/switch_state.mcfunction new file mode 100644 index 000000000..56e9d1fa4 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/run/switch_state.mcfunction @@ -0,0 +1,31 @@ +# Input: +# - Macro: {source: , context: , instance_name: , to: } + +# We use fsm instead of _ to avoid conflicts with lambdas +$data modify storage bs:ctx fsm set value { context: "$(context)", instance_name: "$(instance_name)", state_name: "$(to)" } + +# We unregister the tick command +$data remove storage bs:data fsm.ticks[{instance_name: "$(instance_name)", context: "$(context)"}] + +# We remove the listened transitions for this instance from the list +$data remove storage bs:data fsm.listened_transitions[{instance_name: "$(instance_name)", context: "$(context)"}] + +# We trigger the on_exit command +# Global context +$execute if data storage bs:ctx fsm{context: 'global'} run data modify storage bs:ctx fsm.command set from storage bs:data fsm.running_instances.'$(instance_name)'.states[{name: "$(source)"}].on_exit +execute if data storage bs:ctx fsm{context: 'global'} run function bs.fsm:run/run_command_global with storage bs:ctx fsm +# Local context +$execute unless data storage bs:ctx fsm{context: 'global'} run data modify storage bs:ctx fsm.command set from entity $(context) data.bs:fsm.running_instances.'$(instance_name)'.states[{name: "$(source)"}].on_exit +execute unless data storage bs:ctx fsm{context: 'global'} run function bs.fsm:run/run_command_local with storage bs:ctx fsm + +# We remove the current flag from the previous state +# Global context +$execute if data storage bs:ctx fsm{context: 'global'} run data remove storage bs:data fsm.running_instances.'$(instance_name)'.states[{name: "$(source)"}].current +# Local context +$execute unless data storage bs:ctx fsm{context: 'global'} run data remove entity $(context) data.bs:fsm.running_instances.'$(instance_name)'.states[{name: "$(source)"}].current + +# We enter in the new state +# Global context +execute if data storage bs:ctx fsm{context: 'global'} run function bs.fsm:run/enter_state_global with storage bs:ctx fsm +# Local context +execute unless data storage bs:ctx fsm{context: 'global'} run function bs.fsm:run/enter_state_local with storage bs:ctx fsm diff --git a/modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction index a1fadc885..c1b121a63 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/tick_rec.mcfunction @@ -11,11 +11,11 @@ execute if score #c bs.ctx matches ..0 run return 1 scoreboard players remove #c bs.ctx 1 # If the command is global, we run it -execute unless data storage bs:data fsm.ticks[0].context run function bs.fsm:run/run_command_global with storage bs:data fsm.ticks[0] +execute if data storage bs:data fsm.ticks[0].global run function bs.fsm:run/run_command_global with storage bs:data fsm.ticks[0] scoreboard players set #s bs.ctx -1 # Else, we first check if the entity exists, otherwise we remove the command from the tick list -execute if data storage bs:data fsm.ticks[0].context store success score #s bs.ctx run function bs.fsm:run/check_entity with storage bs:data fsm.ticks[0] +execute unless data storage bs:data fsm.ticks[0].global store success score #s bs.ctx run function bs.fsm:run/check_entity with storage bs:data fsm.ticks[0] execute if score #s bs.ctx matches 0 run data remove storage bs:data fsm.ticks[0] # We run the command only if the entity exists diff --git a/modules/bs.fsm/data/bs.fsm/function/test.mcfunction b/modules/bs.fsm/data/bs.fsm/function/test.mcfunction index 4598b68b2..356b40f72 100644 --- a/modules/bs.fsm/data/bs.fsm/function/test.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/test.mcfunction @@ -1,5 +1,8 @@ execute unless entity B5-0-0-0-9 run summon marker ~ ~ ~ {Tags:["bs.entity", "bs.fsm.test"], UUID:[I;181,0,0,9]} +data modify entity B5-0-0-0-9 data set value {} +data modify storage bs:data fsm set value { fsm: {}, running_instances: {}, listened_transitions: [], ticks: [] } + # Create a traffic light FSM function bs.fsm:new { \ name: "traffic_light", \ @@ -13,7 +16,7 @@ function bs.fsm:new { \ transitions: [ \ { \ name: "to_green", \ - condition: { type: "delay", wait: "20s" }, \ + condition: { type: "delay", wait: 100 }, \ to: "green" \ }, \ { \ @@ -25,12 +28,12 @@ function bs.fsm:new { \ }, \ { \ name: "green", \ - on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"light_green\"},{\"text\":\" Green light - Go!\"}]", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"green\"},{\"text\":\" Green light - Go!\"}]", \ on_tick: "particle minecraft:block{block_state:{Name:\"green_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ transitions: [ \ { \ name: "to_orange", \ - condition: { type: "delay", wait: "30s" }, \ + condition: { type: "delay", wait: 100 }, \ to: "orange" \ }, \ { \ @@ -44,10 +47,11 @@ function bs.fsm:new { \ name: "orange", \ on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"gold\"},{\"text\":\" Orange light - Prepare to stop!\"}]", \ on_tick: "particle minecraft:block{block_state:{Name:\"yellow_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + on_exit: "tellraw @a [{\"text\":\" Reset cycle…\"}]", \ transitions: [ \ { \ name: "to_red", \ - condition: { type: "delay", wait: "5s" }, \ + condition: { type: "delay", wait: 100 }, \ to: "red" \ }, \ { \ diff --git a/modules/bs.fsm/data/bs.fsm/function/todo.md b/modules/bs.fsm/data/bs.fsm/function/todo.md new file mode 100644 index 000000000..89407234e --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/todo.md @@ -0,0 +1,3 @@ +# TODO +- [ ] Change the way to check if a string is inside a list +- [ ] Fix the bug where the tick list is empty after a restart of the world: issue seems to come from the fact that during one tick after a restart, the entity does not exist yet (unloaded chunk?) to the tick function is removed from the list diff --git a/modules/bs.fsm/data/bs.fsm/test/new.mcfunction b/modules/bs.fsm/data/bs.fsm/test/new.mcfunction index 4c8dd3480..7dbeb3754 100644 --- a/modules/bs.fsm/data/bs.fsm/test/new.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/test/new.mcfunction @@ -203,7 +203,7 @@ function #bs.fsm:new { \ }, \ { \ name: "function_transition", \ - condition: { type: "function", wait: "bs.fsm:test/function" }, \ + condition: { type: "command", wait: "bs.fsm:test/function" }, \ to: "processing" \ }, \ { \ From bb2e5af38fbbe12d63f91e4285dc63b9848e5a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Sun, 20 Jul 2025 15:57:01 +0200 Subject: [PATCH 11/14] Some fixes --- .../evaluate_command_transition.mcfunction | 19 ++--- .../evaluate_predicate_transition.mcfunction | 6 +- .../run/evaluate_transitions_rec.mcfunction | 8 +- .../data/bs.fsm/function/test2.mcfunction | 78 +++++++++++++++++++ .../data/bs.fsm/function/test3.mcfunction | 78 +++++++++++++++++++ .../bs.fsm/data/bs.fsm/predicate/test.json | 8 ++ 6 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 modules/bs.fsm/data/bs.fsm/function/test2.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/function/test3.mcfunction create mode 100644 modules/bs.fsm/data/bs.fsm/predicate/test.json diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction index 132c03081..e10b975e1 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_command_transition.mcfunction @@ -1,18 +1,19 @@ -# If this is a global function transition, we run the function and according to the result, we switch to the target state +data modify storage bs:ctx _.tmp set value {} +data modify storage bs:ctx _.tmp.command set from storage bs:data fsm.listened_transitions[0].condition.wait + +# If this is a global command transition, we run the command and according to the result, we switch to the target state execute if data storage bs:data fsm.listened_transitions[0].global \ store success score #s bs.ctx \ - run function bs.fsm:run/run_command_global with storage bs:data fsm.listened_transitions[0].condition + run function bs.fsm:run/run_command_global with storage bs:ctx _.tmp execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] execute if score #s bs.ctx matches 1 run return 1 +execute if data storage bs:data fsm.listened_transitions[0].global run return fail -# If this is a local function transition, we run the function and according to the result, we switch to the target state -data modify storage bs:ctx _.tmp set value {} -data modify storage bs:ctx _.tmp.command set from storage bs:data fsm.listened_transitions[0].condition.wait +# If this is a local command transition, we run the command and according to the result, we switch to the target state data modify storage bs:ctx _.tmp.context set from storage bs:data fsm.listened_transitions[0].context -execute unless data storage bs:data fsm.listened_transitions[0].global \ - store success score #s bs.ctx \ - run function bs.fsm:run/run_command_local with storage bs:data fsm.listened_transitions[0] -execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:ctx _.tmp +execute store success score #s bs.ctx \ + run function bs.fsm:run/run_command_local with storage bs:ctx _.tmp +execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] execute if score #s bs.ctx matches 1 run return 1 return fail diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction index 3a5fc7a6d..5b3e130fb 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_predicate_transition.mcfunction @@ -1,5 +1,5 @@ data modify storage bs:ctx _.transition set from storage bs:data fsm.listened_transitions[0] -data modify storage bs:ctx _.transition.predicate set from storage bs:ctx _.transition.condition.predicate +data modify storage bs:ctx _.transition.predicate set from storage bs:ctx _.transition.condition.wait # If this is a global predicate transition, we evaluate the predicate and according to the result, we switch to the target state execute if data storage bs:data fsm.listened_transitions[0].global \ @@ -7,10 +7,10 @@ execute if data storage bs:data fsm.listened_transitions[0].global \ run function bs.fsm:run/evaluate_global_predicate with storage bs:ctx _.transition execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] execute if score #s bs.ctx matches 1 run return 1 +execute if data storage bs:data fsm.listened_transitions[0].global run return fail # If this is a local predicate transition, we evaluate the predicate and according to the result, we switch to the target state -execute unless data storage bs:data fsm.listened_transitions[0].global \ - store success score #s bs.ctx \ +execute store success score #s bs.ctx \ run function bs.fsm:run/evaluate_local_predicate with storage bs:ctx _.transition execute if score #s bs.ctx matches 1 run function bs.fsm:run/switch_state with storage bs:data fsm.listened_transitions[0] execute if score #s bs.ctx matches 1 run return 1 diff --git a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction index 96e54f00c..9b61d47c0 100644 --- a/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction +++ b/modules/bs.fsm/data/bs.fsm/function/run/evaluate_transitions_rec.mcfunction @@ -16,14 +16,16 @@ execute unless data storage bs:data fsm.listened_transitions[0].global \ execute if score #s bs.ctx matches 0 run data remove storage bs:data fsm.listened_transitions[0] execute if score #s bs.ctx matches 0 run return run function bs.fsm:run/evaluate_transitions_rec +scoreboard players set #s bs.ctx 0 + # Delayed transition execute store success score #s bs.ctx if data storage bs:data fsm.listened_transitions[0].condition{type: "delay"} run function bs.fsm:run/evaluate_delay_transition -# Function transition -execute if score #s bs.ctx matches 0 store success score #s bs.ctx if data storage bs:data fsm.listened_transitions[0].condition{type: "command"} run function bs.fsm:run/evaluate_command_transition +# Command transition +execute if score #s bs.ctx matches 0 if data storage bs:data fsm.listened_transitions[0].condition{type: "command"} store success score #s bs.ctx run function bs.fsm:run/evaluate_command_transition # Predicate transitions -execute if score #s bs.ctx matches 0 store success score #s bs.ctx if data storage bs:data fsm.listened_transitions[0].condition{type: "predicate"} run function bs.fsm:run/evaluate_predicate_transition +execute if score #s bs.ctx matches 0 if data storage bs:data fsm.listened_transitions[0].condition{type: "predicate"} store success score #s bs.ctx run function bs.fsm:run/evaluate_predicate_transition # If we didn't evaluated the current transition, we set the transition as checked and we shift the list execute if score #s bs.ctx matches 0 run data modify storage bs:data fsm.listened_transitions[0].checked set value true diff --git a/modules/bs.fsm/data/bs.fsm/function/test2.mcfunction b/modules/bs.fsm/data/bs.fsm/function/test2.mcfunction new file mode 100644 index 000000000..e611dfe4d --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/test2.mcfunction @@ -0,0 +1,78 @@ +execute unless entity B5-0-0-0-9 run summon marker ~ ~ ~ {Tags:["bs.entity", "bs.fsm.test"], UUID:[I;181,0,0,9]} + +data modify entity B5-0-0-0-9 data set value {} +data modify storage bs:data fsm set value { fsm: {}, running_instances: {}, listened_transitions: [], ticks: [] } + +# Create a traffic light FSM +function bs.fsm:new { \ + name: "traffic_light", \ + fsm: { \ + initial: "red", \ + states: [ \ + { \ + name: "red", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"red\"},{\"text\":\" Red light - Stop!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"red_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + transitions: [ \ + { \ + name: "to_green", \ + condition: { type: "command", wait: "execute if entity @e[tag=bs.fsm.test2]" }, \ + to: "green" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "green", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"green\"},{\"text\":\" Green light - Go!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"green_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + transitions: [ \ + { \ + name: "to_orange", \ + condition: { type: "delay", wait: 100 }, \ + to: "orange" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "orange", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"gold\"},{\"text\":\" Orange light - Prepare to stop!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"yellow_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + on_exit: "tellraw @a [{\"text\":\" Reset cycle…\"}]", \ + transitions: [ \ + { \ + name: "to_red", \ + condition: { type: "delay", wait: 100 }, \ + to: "red" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "off", \ + final: true, \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"black\"},{\"text\":\" Off light - Be careful!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"black_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + } \ + ] \ + } \ +} + +# Start the traffic light FSM +execute as B5-0-0-0-9 run function bs.fsm:start_as { \ + fsm_name: "traffic_light", \ + instance_name: "main_traffic_light" \ +} diff --git a/modules/bs.fsm/data/bs.fsm/function/test3.mcfunction b/modules/bs.fsm/data/bs.fsm/function/test3.mcfunction new file mode 100644 index 000000000..a685472f6 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/function/test3.mcfunction @@ -0,0 +1,78 @@ +execute unless entity B5-0-0-0-9 run summon marker ~ ~ ~ {Tags:["bs.entity", "bs.fsm.test"], UUID:[I;181,0,0,9]} + +data modify entity B5-0-0-0-9 data set value {} +data modify storage bs:data fsm set value { fsm: {}, running_instances: {}, listened_transitions: [], ticks: [] } + +# Create a traffic light FSM +function bs.fsm:new { \ + name: "traffic_light", \ + fsm: { \ + initial: "red", \ + states: [ \ + { \ + name: "red", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"red\"},{\"text\":\" Red light - Stop!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"red_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + transitions: [ \ + { \ + name: "to_green", \ + condition: { type: "predicate", wait: "bs.fsm:test" }, \ + to: "green" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "green", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"green\"},{\"text\":\" Green light - Go!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"green_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + transitions: [ \ + { \ + name: "to_orange", \ + condition: { type: "delay", wait: 100 }, \ + to: "orange" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "orange", \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"gold\"},{\"text\":\" Orange light - Prepare to stop!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"yellow_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + on_exit: "tellraw @a [{\"text\":\" Reset cycle…\"}]", \ + transitions: [ \ + { \ + name: "to_red", \ + condition: { type: "delay", wait: 100 }, \ + to: "red" \ + }, \ + { \ + name: "to_off", \ + condition: "manual", \ + to: "off" \ + } \ + ] \ + }, \ + { \ + name: "off", \ + final: true, \ + on_enter: "tellraw @a [{\"text\":\"⬤\",\"color\":\"black\"},{\"text\":\" Off light - Be careful!\"}]", \ + on_tick: "particle minecraft:block{block_state:{Name:\"black_concrete\"}} ~ ~2 ~ 0.5 0.5 0.5 0 10", \ + } \ + ] \ + } \ +} + +# Start the traffic light FSM +execute as B5-0-0-0-9 run function bs.fsm:start_as { \ + fsm_name: "traffic_light", \ + instance_name: "main_traffic_light" \ +} diff --git a/modules/bs.fsm/data/bs.fsm/predicate/test.json b/modules/bs.fsm/data/bs.fsm/predicate/test.json new file mode 100644 index 000000000..b53de3393 --- /dev/null +++ b/modules/bs.fsm/data/bs.fsm/predicate/test.json @@ -0,0 +1,8 @@ +{ + "condition": "minecraft:time_check", + "value": { + "min": 0, + "max": 12000 + }, + "period": 24000 +} From 3e954220cec919be60a411e075bf601eaaf59718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Sun, 20 Jul 2025 16:47:59 +0200 Subject: [PATCH 12/14] Improve documentation --- docs/_static/bookshelf.css | 5 ++ docs/conf.py | 1 + docs/modules/fsm.md | 180 ++++++++++++++++++++----------------- docs/modules/index.md | 1 + pdm.lock | 17 +++- pyproject.toml | 1 + 6 files changed, 121 insertions(+), 84 deletions(-) diff --git a/docs/_static/bookshelf.css b/docs/_static/bookshelf.css index 5390bd0d5..c46277761 100644 --- a/docs/_static/bookshelf.css +++ b/docs/_static/bookshelf.css @@ -397,3 +397,8 @@ ul.navbar-icon-links { footer, #pst-secondary-sidebar { --pst-color-link: var(--pst-color-text-base) } + +.mermaid { + background-color: var(--pst-color-background); + border: none; +} diff --git a/docs/conf.py b/docs/conf.py index f9e60ca9d..139e4deca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,6 +20,7 @@ "sphinx_minecraft", "sphinx_togglebutton", "sphinx_treeview", + "sphinxcontrib.mermaid", ] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] diff --git a/docs/modules/fsm.md b/docs/modules/fsm.md index 0bd9411e6..6ec44f5f2 100644 --- a/docs/modules/fsm.md +++ b/docs/modules/fsm.md @@ -1,4 +1,4 @@ -# 🔄 FSM (Finite State Machine) +# 🔄 Finite State Machine **`#bs.fsm:help`** @@ -46,7 +46,7 @@ Create a new Finite State Machine (FSM) with the specified configuration. - {nbt}`list` **transitions**: Array of transition definitions (optional). - {nbt}`compound` Transition - {nbt}`string` **name**: Name of the transition (optional). - - {nbt}`string` {nbt}`compound` **condition**: Transition condition. + - {nbt}`string` {nbt}`compound` **condition**: Transition condition. One of the following: - **"manual"**: Manual transition triggered by external call. - {nbt}`compound` **predicate**: Predicate-based transition. - {nbt}`string` **type**: Must be "predicate". @@ -69,65 +69,34 @@ Create a new Finite State Machine (FSM) with the specified configuration. **State**: The FSM is registered and available for use. ``` -*Example: Create a simple door FSM with open/closed states:* +*Example: Create a simple light FSM with on/off states:* ```mcfunction -# Create a door FSM +# Create a light FSM function #bs.fsm:new { \ - name: "door_fsm", \ + name: "light_fsm", \ fsm: { \ - initial: "closed", \ - on_cancel: "bs.door:cancel", \ + initial: "off", \ states: [ \ { \ - name: "closed", \ - on_tick: "bs.door:closed_tick", \ - on_enter: "bs.door:close_door", \ - on_exit: "bs.door:prepare_open", \ + name: "off", \ + on_enter: "setblock ~ ~ ~ minecraft:redstone_lamp", \ transitions: [ \ { \ - name: "open", \ + name: "turn_on", \ condition: "manual", \ - to: "opening" \ + to: "on" \ } \ ] \ }, \ { \ - name: "opening", \ - on_tick: "bs.door:opening_tick", \ - on_enter: "bs.door:start_opening", \ - on_exit: "bs.door:finish_opening", \ + name: "on", \ + on_enter: "setblock ~ ~ ~ minecraft:redstone_lamp[lit=true]", \ transitions: [ \ { \ - name: "opened", \ - condition: { type: "delay", wait: "20t" }, \ - to: "open" \ - } \ - ] \ - }, \ - { \ - name: "open", \ - on_tick: "bs.door:open_tick", \ - on_enter: "bs.door:open_door", \ - on_exit: "bs.door:prepare_close", \ - transitions: [ \ - { \ - name: "close", \ + name: "turn_off", \ condition: "manual", \ - to: "closing" \ - } \ - ] \ - }, \ - { \ - name: "closing", \ - on_tick: "bs.door:closing_tick", \ - on_enter: "bs.door:start_closing", \ - on_exit: "bs.door:finish_closing", \ - transitions: [ \ - { \ - name: "closed", \ - condition: { type: "delay", wait: "20t" }, \ - to: "closed" \ + to: "off" \ } \ ] \ } \ @@ -142,8 +111,8 @@ function #bs.fsm:new { \ ### Start -```{tab-set} -```{tab-item} Global Instance +:::::{tab-set} +::::{tab-item} Global Instance ```{function} #bs.fsm:start @@ -163,24 +132,25 @@ Start a new global instance of a Finite State Machine. **State**: The FSM instance is created globally and begins execution in its initial state. ``` -*Example: Start a door FSM instance:* +*Example: Start a light FSM instance:* ```mcfunction -# Start a door FSM instance -function #bs.fsm:start { fsm_name: "door_fsm", instance_name: "main_door" } +# Start a light FSM instance +function #bs.fsm:start { fsm_name: "light_fsm", instance_name: "main_light" } -# The door FSM is now running globally and will execute its initial state +# The light FSM is now running globally and will execute its initial state ``` > **Credits**: theogiraudet -``` - -```{tab-item} Local Instance +:::: +::::{tab-item} Local Instance ```{function} #bs.fsm:start_as Start new local instances of a Finite State Machine bound to the executing entities. +The different commands and predicates used in the FSM will be executed as and at the executing entities. +If the entity is killed during the execution of the FSM, the module will automatically stop the tick commands and transitions evaluation for this entity. :Inputs: **Execution `as `**: Entities to bind. The entities must not be players. @@ -189,7 +159,7 @@ Start new local instances of a Finite State Machine bound to the executing entit :::{treeview} - {nbt}`compound` Arguments - {nbt}`string` **fsm_name**: Name of the FSM to instantiate (must exist). - - {nbt}`string` **instance_name**: Unique identifier for this FSM instance. + - {nbt}`string` **instance_name**: Unique identifier for this FSM instance in this context. ::: :Outputs: @@ -198,19 +168,19 @@ Start new local instances of a Finite State Machine bound to the executing entit **State**: The FSM instances are created locally for the executing entities and begins execution in their initial state. ``` -*Example: Start a door FSM instance for an entity:* +*Example: Start a light FSM instance for an entity:* ```mcfunction -# Start a door FSM instance bound to the executing entity -execute as @n[type=zombie] run function #bs.fsm:start_as { fsm_name: "door_fsm", instance_name: "entity_door" } +# Start a light FSM instance bound to the executing entity +execute as @n[type=zombie] run function #bs.fsm:start_as { fsm_name: "light_fsm", instance_name: "entity_light" } -# The door FSM is now running locally for this zombie and will execute its initial state +# The light FSM is now running locally for this zombie and will execute its initial state ``` > **Credits**: theogiraudet -``` -``` +:::: +::::: --- @@ -281,6 +251,59 @@ function #bs.fsm:delete { fsm_name: "door_fsm" } --- +## ❓ What is a FSM? + +A Finite State Machine (FSM) is a conceptual model used to describe how a system behaves in response to events. +It defines a limited set of possible states that the system can be in at any given moment. +The system starts in an initial state and, when something happens, such as receiving an input or a signal, it may change its state following predefined rules. +These changes are called transitions, and each one depends on the current state and the event received. + +What makes FSMs powerful is their simplicity and clarity. +By reducing a system's behavior to a set of states and transitions, we can describe even complex logic in a very structured and predictable way. +At any point in time, the system is in exactly one state, and the logic for moving between states is well defined. +This helps avoid ambiguity and makes it easier to understand how the system reacts to different situations. + +In Minecraft, Finite State Machines can be particularly useful to manage tree dialog, boss phases, or any system state. +Outside Minecraft, Finite State Machines are widely used in many fields because they provide a clean way to manage systems that have different modes or stages. +In software development, they are useful for designing user interfaces, game character behavior, communication protocols, and more. +In hardware and control systems, they are often used to manage sequences of operations or reactions to sensor inputs. +Overall, FSMs are a fundamental tool for modeling reactive systems in a way that is both rigorous and easy to reason about. + +## 💡 Example in Minecraft + +```{mermaid} +stateDiagram-v2 + [*] --> Idle + + Idle --> Alert : if player detected + + Alert --> Attack : after 5s AND player still detected + Alert --> Idle : after 5s AND player gone + + Attack --> Searching : if player lost + + Searching --> Attack : if player found + Searching --> Idle : after 10s AND player not found + + Attack --> Idle : if player defeated +``` + +This finite state machine controls the behavior of a custom mob in Minecraft: a sentinel that guards a specific area. +It begins in the **Idle** state, where it stays mostly still, occasionally performing small ambient animations. +When a player enters its detection radius, as determined by a custom command or predicate, the FSM transitions to the **Alert** state. +In the **Alert** state, the sentinel visually or audibly signals that it has detected an intruder. +This state is time-based, lasting about five seconds. +If the player is still present when this period ends, the sentinel moves to the **Attack** state. +During **Attack**, the mob actively pursues and attacks the player. +If the player escapes or is no longer detectable, the FSM transitions to the **Searching** state. +There, the sentinel wanders the area near the last known location of the intruder for a set amount of time. +If it finds the player again during this search, it returns to **Attack**. +Otherwise, if the timer runs out without detecting anyone, it returns to the **Idle** state and resumes its guard duty. +If the player is defeated, the FSM transitions to the **Idle** state and resumes its guard duty. + + + +--- ## 📋 Validation Rules The FSM system enforces several validation rules to ensure proper operation: @@ -313,7 +336,7 @@ Each state in an FSM follows a specific lifecycle: 1. **Enter**: The `on_enter` function is called when entering the state 2. **Tick**: The `on_tick` function is called every tick while in the state -3. **Transition**: When a transition condition is met, the state transitions +3. **Transition evaluation**: When a transition condition is met, the state transitions 4. **Exit**: The `on_exit` function is called when leaving the state --- @@ -323,32 +346,24 @@ Each state in an FSM follows a specific lifecycle: The FSM system supports several types of transitions: ### Manual -Triggered by external function calls. Useful for player interactions or external events. +Triggered by external function calls. +Useful for player interactions or external events. ### Predicate -Triggered when a predicate function returns true. Useful for conditional logic. +Triggered when a predicate returns true. +Useful for conditional logic. -### Function -Triggered when a function returns a specific value. Useful for complex conditions. +### Command +Triggered when a command succeeds. +Useful for complex conditions. ### Hook -Triggered by hook system events. Useful for integration with other systems. +Triggered by hook system events. +Useful for integration with other systems. ### Delay -Triggered after a specified time delay. Useful for timed behaviors. - ---- - -## 🎯 Use Cases - -FSMs are particularly useful for: - -- **Entity AI**: Managing complex behavior patterns -- **Machine States**: Controlling redstone contraptions -- **Game Mechanics**: Implementing complex game logic -- **UI Systems**: Managing interface states -- **Animation Systems**: Controlling entity animations -- **Quest Systems**: Managing quest progression +Triggered after a specified time delay. +Useful for timed behaviors. --- @@ -358,5 +373,4 @@ FSMs are particularly useful for: 2. **Use meaningful names**: State and transition names should clearly describe their purpose 3. **Handle edge cases**: Always consider what happens when transitions fail 4. **Clean up resources**: Use the on_exit functions to clean up state-specific resources -5. **Test thoroughly**: FSMs can become complex, so comprehensive testing is essential -6. **Document transitions**: Clearly document when and why transitions occur +5. **Define a cancel command**: Use a `on_cancel` command to clean up resources when the FSM is cancelled diff --git a/docs/modules/index.md b/docs/modules/index.md index 957886683..7f60f3216 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -30,6 +30,7 @@ bitwise block color environment +fsm generation health hitbox diff --git a/pdm.lock b/pdm.lock index c0a75b84b..8bce53fd8 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev", "docs"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:4987885735af3eab09577d99066db9d152c136903d311221a97fa0cacea6fe98" +content_hash = "sha256:85b4f8c6c53c930bdbccb7943e0ff75a9c0368664466274e155774042b1f20f4" [[metadata.targets]] requires_python = ">=3.12" @@ -894,6 +894,21 @@ files = [ {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] +[[package]] +name = "sphinxcontrib-mermaid" +version = "1.0.0" +requires_python = ">=3.8" +summary = "Mermaid diagrams in yours Sphinx powered docs" +groups = ["docs"] +dependencies = [ + "pyyaml", + "sphinx", +] +files = [ + {file = "sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3"}, + {file = "sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146"}, +] + [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" diff --git a/pyproject.toml b/pyproject.toml index dddbc4112..862b0f80a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ docs = [ "sphinx-design>=0.6.1", "sphinx-minecraft>=1.0.2", "sphinx-togglebutton>=0.3.2", + "sphinxcontrib.mermaid>=1.0.0", "sphinx>=8.2.3", ] From 1bc3ccc94f7be3b427bd77c3e8e8c2b5dd3455da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Giraudet?= Date: Sun, 20 Jul 2025 17:08:04 +0200 Subject: [PATCH 13/14] Improve manual API --- docs/modules/fsm.md | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/modules/fsm.md b/docs/modules/fsm.md index 6ec44f5f2..4617abad3 100644 --- a/docs/modules/fsm.md +++ b/docs/modules/fsm.md @@ -47,17 +47,19 @@ Create a new Finite State Machine (FSM) with the specified configuration. - {nbt}`compound` Transition - {nbt}`string` **name**: Name of the transition (optional). - {nbt}`string` {nbt}`compound` **condition**: Transition condition. One of the following: - - **"manual"**: Manual transition triggered by external call. - - {nbt}`compound` **predicate**: Predicate-based transition. + - {nbt}`compound` Predicate-based transition. + - {nbt}`string` **type**: Must be "manual". + - {nbt}`string` **wait**: A signal sent manually to the FSM using the `#bs.fsm:emit` feature. + - {nbt}`compound` Predicate-based transition. - {nbt}`string` **type**: Must be "predicate". - {nbt}`string` **wait**: Predicate to check to trigger the transition. - - {nbt}`compound` **command**: Command-based transition. + - {nbt}`compound` Command-based transition. - {nbt}`string` **type**: Must be "command". - {nbt}`string` **wait**: Command to check to trigger the transition. - - {nbt}`compound` **hook**: Hook-based transition. + - {nbt}`compound` Hook-based transition. - {nbt}`string` **type**: Must be "hook". - {nbt}`string` **wait**: Hook function to evaluate. - - {nbt}`compound` **delay**: Time-based transition. + - {nbt}`compound` Time-based transition. - {nbt}`string` **type**: Must be "delay". - {nbt}`string` **wait**: Time delay in ticks. - {nbt}`string` **to**: Name of the target state (must exist in states array). @@ -184,6 +186,31 @@ execute as @n[type=zombie] run function #bs.fsm:start_as { fsm_name: "light_fsm" --- +### Emit + +```{function} #bs.fsm:emit + +Emit a signal to a running FSM instance. +This signal may or may not trigger a transition, according to the current state of the FSM instance. + +:Inputs: + **Function macro**: + :::{treeview} + - {nbt}`compound` Arguments + - {nbt}`string` **instance_name**: Name of the FSM instance to emit the signal to. + - {nbt}`string` **signal**: Name of the signal to emit. + ::: +``` + +*Example: Emit a signal to a FSM instance:* + +```mcfunction +# Emit a signal to a global FSM instance +function #bs.fsm:emit { instance_name: "main_light", signal: "turn_on" } +``` + +--- + ### Cancel ```{function} #bs.fsm:cancel From 4651191b75af8f56e4914188c1b1369defb6fac3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:15:43 +0000 Subject: [PATCH 14/14] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20chore:=20update?= =?UTF-8?q?=20generated=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/manifest.json | 3 +++ uv.lock | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/data/manifest.json b/data/manifest.json index a5e664a98..c34e0e8d5 100644 --- a/data/manifest.json +++ b/data/manifest.json @@ -1146,6 +1146,7 @@ "features": [ { "id": "#bs.fsm:new", + "kind": "function_tag", "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#new", "authors": [ "theogiraudet" @@ -1161,6 +1162,7 @@ }, { "id": "#bs.fsm:start", + "kind": "function_tag", "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#start", "authors": [ "theogiraudet" @@ -1176,6 +1178,7 @@ }, { "id": "#bs.fsm:start_as", + "kind": "function_tag", "documentation": "https://docs.mcbookshelf.dev/en/latest/modules/fsm.html#start-as", "authors": [ "theogiraudet" diff --git a/uv.lock b/uv.lock index bcc1ebc28..611453b48 100644 --- a/uv.lock +++ b/uv.lock @@ -367,6 +367,7 @@ docs = [ { name = "sphinx-intl" }, { name = "sphinx-minecraft" }, { name = "sphinx-togglebutton" }, + { name = "sphinxcontrib-mermaid" }, ] [package.metadata] @@ -398,6 +399,7 @@ docs = [ { name = "sphinx-intl", specifier = ">=2.3.1" }, { name = "sphinx-minecraft", specifier = ">=1.0.5" }, { name = "sphinx-togglebutton", specifier = ">=0.3.2" }, + { name = "sphinxcontrib-mermaid", specifier = ">=1.0.0" }, ] [[package]] @@ -941,6 +943,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] +[[package]] +name = "sphinxcontrib-mermaid" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153, upload-time = "2024-10-12T16:33:03.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/c8/784b9ac6ea08aa594c1a4becbd0dbe77186785362e31fd633b8c6ae0197a/sphinxcontrib_mermaid-1.0.0-py3-none-any.whl", hash = "sha256:60b72710ea02087f212028feb09711225fbc2e343a10d34822fe787510e1caa3", size = 9597, upload-time = "2024-10-12T16:33:02.303Z" }, +] + [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0"