From efaa130170c932c2cc41a86c185da6c95371be7d Mon Sep 17 00:00:00 2001 From: hugou74 Date: Sun, 3 May 2026 13:59:31 +0000 Subject: [PATCH 1/7] feat(SemanticWorkflow): add loops This commit adds loop functionality to the Semantic Workflow, allowing inputs to jump back to earlier inputs within the same section. Features: - Loop definition with count and jump target - Loop toggle button with icon in InputsTab - Loop count numberbox control - Loop target selection via special select handler - Loop counter reset on sheet run - Loop target highlighting in section drawing (orange border) - Loop source highlighting in section drawing (blue border) - Localization support (en_US, fr_FR) - Custom loop icon for light/dark themes Cleanup from original draft: - Removed debug print statements - Removed TODO comment about 'unclean' IndexOf usage - Added proper any_changes tracking for loop controls - Added run_to_preview() call after target selection - Extracted SectionInputs.new() into proper implementation file References: #109, #112 --- src/core/Locales.lua | 2 + src/res/lang/en_US.lua | 2 + src/res/lang/fr_FR.lua | 2 + .../Definitions/SectionInputs.lua | 15 +++++ .../Implementations/InputListGui.lua | 17 +++++ .../Implementations/InputsTab.lua | 62 +++++++++++++++++- .../Implementations/Section.lua | 5 +- .../Implementations/SectionInputs.lua | 22 +++++++ .../Implementations/Sheet.lua | 38 ++++++++--- src/views/SemanticWorkflow/Main.lua | 2 +- src/views/SemanticWorkflow/Resources/loop.png | Bin 0 -> 623 bytes .../SemanticWorkflow/Resources/loop_light.png | Bin 0 -> 550 bytes 12 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 src/views/SemanticWorkflow/Implementations/SectionInputs.lua create mode 100644 src/views/SemanticWorkflow/Resources/loop.png create mode 100644 src/views/SemanticWorkflow/Resources/loop_light.png diff --git a/src/core/Locales.lua b/src/core/Locales.lua index 2f3f6be..353102a 100644 --- a/src/core/Locales.lua +++ b/src/core/Locales.lua @@ -98,6 +98,8 @@ Locales = {} ---@field public SEMANTIC_WORKFLOW_INPUTS_END_ACTION string ---@field public SEMANTIC_WORKFLOW_INPUTS_END_ACTION_TOOL_TIP string ---@field public SEMANTIC_WORKFLOW_INPUTS_END_ACTION_TYPE_TO_SEARCH_TOOL_TIP string +---@field public SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET string +---@field public SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET_TOOL_TIP string ---@field public SEMANTIC_WORKFLOW_PREFERENCES_EDIT_ENTIRE_STATE string ---@field public SEMANTIC_WORKFLOW_PREFERENCES_FAST_FORWARD string ---@field public SEMANTIC_WORKFLOW_PREFERENCES_DEFAULT_SECTION_TIMEOUT string diff --git a/src/res/lang/en_US.lua b/src/res/lang/en_US.lua index efd2d96..d99552d 100644 --- a/src/res/lang/en_US.lua +++ b/src/res/lang/en_US.lua @@ -116,6 +116,8 @@ This action cannot be undone. SEMANTIC_WORKFLOW_INPUTS_END_ACTION = 'End action:', SEMANTIC_WORKFLOW_INPUTS_END_ACTION_TOOL_TIP = 'End section when Mario enters this action', SEMANTIC_WORKFLOW_INPUTS_END_ACTION_TYPE_TO_SEARCH_TOOL_TIP = 'Type to filter actions', + SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET = 'Target', + SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET_TOOL_TIP = 'Click an input to set it as the loop jump target', SEMANTIC_WORKFLOW_PREFERENCES_TAB_NAME = 'Preferences', SEMANTIC_WORKFLOW_PREFERENCES_EDIT_ENTIRE_STATE = 'Edit entire state', SEMANTIC_WORKFLOW_PREFERENCES_FAST_FORWARD = 'Fast Forward', diff --git a/src/res/lang/fr_FR.lua b/src/res/lang/fr_FR.lua index dcb33f6..7d9f61e 100644 --- a/src/res/lang/fr_FR.lua +++ b/src/res/lang/fr_FR.lua @@ -118,6 +118,8 @@ Cette action est irréversible. SEMANTIC_WORKFLOW_INPUTS_END_ACTION = 'Action de fin :', SEMANTIC_WORKFLOW_INPUTS_END_ACTION_TOOL_TIP = 'Terminer la section quand Mario entre dans cette action', SEMANTIC_WORKFLOW_INPUTS_END_ACTION_TYPE_TO_SEARCH_TOOL_TIP = 'Taper pour filtrer les actions', + SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET = 'Cible', + SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET_TOOL_TIP = 'Cliquer un input pour le définir comme cible du saut de loop', SEMANTIC_WORKFLOW_PREFERENCES_EDIT_ENTIRE_STATE = 'Modifier l\'état entier', SEMANTIC_WORKFLOW_PREFERENCES_FAST_FORWARD = 'Avance rapide', SEMANTIC_WORKFLOW_PREFERENCES_DEFAULT_SECTION_TIMEOUT = 'Délai de section par défaut :', diff --git a/src/views/SemanticWorkflow/Definitions/SectionInputs.lua b/src/views/SemanticWorkflow/Definitions/SectionInputs.lua index 0bfdc5c..487fe8c 100644 --- a/src/views/SemanticWorkflow/Definitions/SectionInputs.lua +++ b/src/views/SemanticWorkflow/Definitions/SectionInputs.lua @@ -4,10 +4,25 @@ -- SPDX-License-Identifier: GPL-2.0-or-later -- +---@class Loop Runtime information for a section loop. +---@field jump_target SectionInputs The input to jump to. Can be the same input as the looping input, but not any later input in a section. +---@field count integer The number of repetitions. May be zero. +---@field runtime_counter integer The current repetition during a sheet's run. +local cls_loop = {} + ---@class SectionInputs Describes the inputs to be made for one or more frames semantically. ---@field end_action integer|nil The 32-bit representation of Mario's action that, when reached in playback, terminates this input. ---@field timeout integer The maximum number of frames this input is held for. end_action may cause an earlier termination. ---@field tas_state table The TAS state to derive the control stick inputs from, which behaves mostly like the controls in the "TAS" view. ---@field joy table The joypad data, that is, pressed buttons and joystick values (joystick values only apply when the tas_state's movement_mode is set to "manual"). ---@field editing boolean Whether the input is selected for editing. +---@field loop Loop|nil The loop info to apply after this input, if applicable. local cls_section_inputs = {} + +function cls_section_inputs.new() end + +__impl = cls_section_inputs +dofile(views_path .. 'SemanticWorkflow/Implementations/SectionInputs.lua') +__impl = nil + +return cls_section_inputs diff --git a/src/views/SemanticWorkflow/Implementations/InputListGui.lua b/src/views/SemanticWorkflow/Implementations/InputListGui.lua index 79f5db8..7fc57cd 100644 --- a/src/views/SemanticWorkflow/Implementations/InputListGui.lua +++ b/src/views/SemanticWorkflow/Implementations/InputListGui.lua @@ -275,6 +275,15 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) end end + local loop_targets = {} + for _, section in ipairs(sheet.sections) do + for _, inp in ipairs(section.inputs) do + if inp.loop and inp.loop.jump_target then + loop_targets[inp.loop.jump_target] = true + end + end + end + iterate_input_rows(sheet, function(section, input, section_index, input_index, row_count) if row_count <= scroll_offset then return false end @@ -490,6 +499,14 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) BreitbandGraphics.draw_ellipse(rect, input.joy[v.input] and '#000000FF' or '#00000050', 1) end + if loop_targets[input] then + BreitbandGraphics.draw_rectangle(section_rect, '#FF8000FF', 2) + end + + if input.loop then + BreitbandGraphics.draw_rectangle(section_rect, '#0064FFFF', 2) + end + if section_index == sheet.preview_input.section_index and sheet.preview_input.input_index == input_index then BreitbandGraphics.draw_rectangle(section_rect, '#FF0000FF', 1) end diff --git a/src/views/SemanticWorkflow/Implementations/InputsTab.lua b/src/views/SemanticWorkflow/Implementations/InputsTab.lua index 5e2bbe9..7ebdeac 100644 --- a/src/views/SemanticWorkflow/Implementations/InputsTab.lua +++ b/src/views/SemanticWorkflow/Implementations/InputsTab.lua @@ -65,6 +65,9 @@ local UID = UIDProvider.allocate_once('InputsTab', function(enum_next) EndAction = enum_next(), EndActionTextbox = enum_next(), AvailableActions = enum_next(MAX_ACTION_GUESSES), + LoopToggle = enum_next(), + LoopSelectTarget = enum_next(), + LoopCount = enum_next(), } end) @@ -120,6 +123,60 @@ local function controls_for_end_action(input, draw, column, top) end end +---@param input SectionInputs +---@return boolean any_changes +local function controls_for_loop(input, draw, column, top) + local any_changes = false + local had_loop = input.loop ~= nil + local has_loop = ugui.toggle_button({ + uid = UID.LoopToggle, + rectangle = grid_rect(column, top, Gui.MEDIUM_CONTROL_HEIGHT, Gui.MEDIUM_CONTROL_HEIGHT), + text = "[icon:loop]", + is_checked = had_loop, + styler_mixin = { icon_size = 14 }, + }) + if not has_loop then + input.loop = nil + elseif input.loop == nil then + input.loop = { + count = 1, + jump_target = input, + runtime_counter = 0, + } + end + any_changes = any_changes or (had_loop ~= (input.loop ~= nil)) + + if input.loop then + local old_count = input.loop.count + input.loop.count = ugui.numberbox({ + uid = UID.LoopCount, + rectangle = grid_rect(column + 1, top, 2, Gui.MEDIUM_CONTROL_HEIGHT), + places = 2, + value = input.loop.count, + }) + any_changes = any_changes or old_count ~= input.loop.count + + if ugui.button({ + uid = UID.LoopSelectTarget, + rectangle = grid_rect(column + 3, top, 3, Gui.MEDIUM_CONTROL_HEIGHT), + text = Locales.str("SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET"), + tooltip = Locales.str("SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET_TOOL_TIP"), + }) then + InputListGui.special_select_handler = function(selection) + local input_list = SemanticWorkflowProject.current.sections[selection.section_index].inputs + local own_index = IndexOf(input_list, input) + if own_index >= selection.input_index then + input.loop.jump_target = input_list[selection.input_index] + InputListGui.special_select_handler = nil + SemanticWorkflowProject:asserted_current():run_to_preview() + end + end + end + end + + return any_changes +end + local function section_controls_for_selected(draw, edited_input) local sheet = SemanticWorkflowProject:asserted_current() @@ -145,6 +202,10 @@ local function section_controls_for_selected(draw, edited_input) controls_for_end_action(edited_input, draw, 0, top) + if end_action_search_text == nil then + any_changes = any_changes or controls_for_loop(edited_input, draw, 0, top + 1) + end + if any_changes then sheet:run_to_preview() end @@ -244,7 +305,6 @@ local function select_atan_end(selection_input) end local function select_atan_start(selection_input) - print(selection_input) local sheet = SemanticWorkflowProject:asserted_current() previous_preview_input = sheet.preview_input sheet.preview_input = selection_input diff --git a/src/views/SemanticWorkflow/Implementations/Section.lua b/src/views/SemanticWorkflow/Implementations/Section.lua index fd8f657..d203650 100644 --- a/src/views/SemanticWorkflow/Implementations/Section.lua +++ b/src/views/SemanticWorkflow/Implementations/Section.lua @@ -8,11 +8,14 @@ ---@diagnostic disable-next-line:assign-type-mismatch local __impl = __impl +---@type SectionInputs +local SectionInputs = dofile(views_path .. 'SemanticWorkflow/Definitions/SectionInputs.lua') + function __impl.new(name) local tmp = {} CloneInto(tmp, Joypad.input) return { - inputs = { { tas_state = NewTASState(), joy = tmp, timeout = 1, end_action = 0 } }, + inputs = { SectionInputs.new() }, collapsed = false, name = name } diff --git a/src/views/SemanticWorkflow/Implementations/SectionInputs.lua b/src/views/SemanticWorkflow/Implementations/SectionInputs.lua new file mode 100644 index 0000000..5b4640e --- /dev/null +++ b/src/views/SemanticWorkflow/Implementations/SectionInputs.lua @@ -0,0 +1,22 @@ +-- +-- Copyright (c) 2025, Mupen64 maintainers. +-- +-- SPDX-License-Identifier: GPL-2.0-or-later +-- + +---@type SectionInputs +---@diagnostic disable-next-line:assign-type-mismatch +local __impl = __impl + +function __impl.new() + local tmp = {} + CloneInto(tmp, Joypad.input) + ---@type SectionInputs + return { + tas_state = NewTASState(), + joy = tmp, + timeout = 1, + end_action = 0, + editing = false, + } +end diff --git a/src/views/SemanticWorkflow/Implementations/Sheet.lua b/src/views/SemanticWorkflow/Implementations/Sheet.lua index 30bcfae..364be3f 100644 --- a/src/views/SemanticWorkflow/Implementations/Sheet.lua +++ b/src/views/SemanticWorkflow/Implementations/Sheet.lua @@ -65,13 +65,18 @@ function __impl:evaluate_frame() if (input.timeout and self._frame_counter >= input.timeout) or current_action == input.end_action then - self._input_index = self._input_index + 1 self._frame_counter = 0 - if #section.inputs < self._input_index then - self.measured_section_lengths[section] = self._section_frame_counter - self._section_frame_counter = 0 - self._section_index = self._section_index + 1 - self._input_index = 1 + if input.loop == nil or input.loop.runtime_counter >= input.loop.count then + self._input_index = self._input_index + 1 + if #section.inputs < self._input_index then + self.measured_section_lengths[section] = self._section_frame_counter + self._section_frame_counter = 0 + self._section_index = self._section_index + 1 + self._input_index = 1 + end + else + input.loop.runtime_counter = input.loop.runtime_counter + 1 + self._input_index = IndexOf(section.inputs, input.loop.jump_target) or 1 end end if self._section_index > self.preview_input.section_index @@ -101,15 +106,28 @@ function __impl:evaluate_frame() end ---@param sheet Sheet ----@param from_base boolean | nil -local function run_to_preview_internal(sheet, from_base) - sheet.busy = true - +local function reset_counters(sheet) sheet._section_index = 1 sheet._input_index = 1 sheet._frame_counter = 0 sheet._section_frame_counter = 0 + -- reset loop counters + for _, section in pairs(sheet.sections) do + for _, input in pairs(section.inputs) do + if input.loop then + input.loop.runtime_counter = 0 + end + end + end +end + +---@param sheet Sheet +---@param from_base boolean | nil +local function run_to_preview_internal(sheet, from_base) + sheet.busy = true + reset_counters(sheet) + if from_base == nil or from_base then if sheet._base_sheet ~= nil then if sheet._savestate == nil or sheet._base_sheet:invalidated() then diff --git a/src/views/SemanticWorkflow/Main.lua b/src/views/SemanticWorkflow/Main.lua index e3ccc8c..5f8f689 100644 --- a/src/views/SemanticWorkflow/Main.lua +++ b/src/views/SemanticWorkflow/Main.lua @@ -90,7 +90,7 @@ SemanticWorkflowDialog = nil local ugui_icon_draw = ugui.standard_styler.draw_icon local custom_icons = { 'navigate_back', 'arrow_up', 'arrow_down', 'base_sheet', 'without_save', 'delete', - 'next_page', 'previous_page', 'duplicate', 'action', 'clone_up', 'clone_down', 'merge_up'} + 'next_page', 'previous_page', 'duplicate', 'action', 'clone_up', 'clone_down', 'merge_up', 'loop'} ugui.standard_styler.draw_icon = function(rectangle, color, visual_state, key) local postfix = Drawing.IsLightMode() and '' or '_light' diff --git a/src/views/SemanticWorkflow/Resources/loop.png b/src/views/SemanticWorkflow/Resources/loop.png new file mode 100644 index 0000000000000000000000000000000000000000..78f3a3eb4716820224912058e7dfd85ef9ff3815 GIT binary patch literal 623 zcmV-#0+9WQP)HsOUneqU1lIqN9tBj*iYIDN;H*Ro3qrnK`p7 z8&2GE=E*zn^?B!#NxAgHpT8$C#!SRhAc8fS5hulCUf6t2L$6;AowQ=|s+V;Y|& zHpbNCG{)2g%^eB}2v`Dkbi3WPz69-ywgMAW_HMo_Wb(5ail3R1LoZ zZqup^+@3SZVLHO(DONOBEWG}`aYUtOzkU-85JR9~82AjX*tio*% z=KkZ;1TLM9r|A4Y`U~?h0@?kEP)09}sZEnMB$_npKhUHN8#ZX#v`HhfY_#jQ$2ph##)U7| z-kh29+IQ~Au~Pcw&;JVqK~RNHFo`~nwOKD9_XHHHaX^C?nANC(--KP9nzo2PM6LiC z+t9F6PJ9R)LxY4a*qnJ{K9MUxzYZxatZ>UZ2Ar3$jPn2rNBT-KCcyFbp|Gw*8a&7r zk(_Jdt1uHzfR6UQuY6X|+d@N~-ns7SEibV`(uklg5;Fz<_H;M@N+)P0%1`}X7v`7Q zhS$@9DMYg?M2Aq9<@~JASvoG6YR+W@8G=X`ocqROQ8Ta`sketktgx4*(O>;uYpyB0^VW2i7o}p z-WN0Y$504|A)6(V?CEQ)L&OBU^1Y9aD0DN)@AbQ=vR1Paxo!aZoH2gk2LG!T#+ZPd zBy3@A$n|KTQG(0CM#={M2Gks~xuxd2E-Uht5>O107S>*uo0K$*&L%bOlVfO7lWMMD zrhozz44^R49=^jQYD$3Nr6v<{1(Jz0v41e(-#9rs2__$37O)lw)mbmX3F{SXAwB>A o0RR6K4|W9r000I_L_t&o08gyOD&uTE9{>OV07*qoM6N<$f>`?Uu>b%7 literal 0 HcmV?d00001 From 2e291eb4400ce443c5f8ebe2274173b051f0ea0e Mon Sep 17 00:00:00 2001 From: hugou74 Date: Sun, 3 May 2026 14:12:42 +0000 Subject: [PATCH 2/7] fix(loop): address PR review blockers - Replace jump_target object reference with 1-based index to fix save/load serialization (circular reference in json.encode) - Add runtime guards in Sheet.evaluate_frame(): - validate target_index is within bounds and not a forward jump - handle nil runtime_counter safely - support count=0 as while-like infinite loop - Fix InputListGui highlighting to use composite keys (section_index:input_index) instead of object reference keys - Add section guard in InputsTab loop target selector - Initialize runtime_counter after load() if absent --- .../Definitions/SectionInputs.lua | 2 +- .../Implementations/InputListGui.lua | 6 ++-- .../Implementations/InputsTab.lua | 24 +++++++++++--- .../Implementations/Sheet.lua | 31 +++++++++++++++++-- 4 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/views/SemanticWorkflow/Definitions/SectionInputs.lua b/src/views/SemanticWorkflow/Definitions/SectionInputs.lua index 487fe8c..1ca6a18 100644 --- a/src/views/SemanticWorkflow/Definitions/SectionInputs.lua +++ b/src/views/SemanticWorkflow/Definitions/SectionInputs.lua @@ -5,7 +5,7 @@ -- ---@class Loop Runtime information for a section loop. ----@field jump_target SectionInputs The input to jump to. Can be the same input as the looping input, but not any later input in a section. +---@field jump_target integer The 1-based index of the input to jump to within the same section. Can be the current input's index (self-loop), but must not be larger than the looping input's index. ---@field count integer The number of repetitions. May be zero. ---@field runtime_counter integer The current repetition during a sheet's run. local cls_loop = {} diff --git a/src/views/SemanticWorkflow/Implementations/InputListGui.lua b/src/views/SemanticWorkflow/Implementations/InputListGui.lua index 7fc57cd..a30f204 100644 --- a/src/views/SemanticWorkflow/Implementations/InputListGui.lua +++ b/src/views/SemanticWorkflow/Implementations/InputListGui.lua @@ -276,10 +276,10 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) end local loop_targets = {} - for _, section in ipairs(sheet.sections) do + for s_idx, section in ipairs(sheet.sections) do for _, inp in ipairs(section.inputs) do if inp.loop and inp.loop.jump_target then - loop_targets[inp.loop.jump_target] = true + loop_targets[s_idx .. ":" .. inp.loop.jump_target] = true end end end @@ -499,7 +499,7 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) BreitbandGraphics.draw_ellipse(rect, input.joy[v.input] and '#000000FF' or '#00000050', 1) end - if loop_targets[input] then + if loop_targets[section_index .. ":" .. input_index] then BreitbandGraphics.draw_rectangle(section_rect, '#FF8000FF', 2) end diff --git a/src/views/SemanticWorkflow/Implementations/InputsTab.lua b/src/views/SemanticWorkflow/Implementations/InputsTab.lua index 7ebdeac..47b9e4a 100644 --- a/src/views/SemanticWorkflow/Implementations/InputsTab.lua +++ b/src/views/SemanticWorkflow/Implementations/InputsTab.lua @@ -138,9 +138,14 @@ local function controls_for_loop(input, draw, column, top) if not has_loop then input.loop = nil elseif input.loop == nil then + local own_index = nil + for _, section in ipairs(SemanticWorkflowProject:asserted_current().sections) do + own_index = IndexOf(section.inputs, input) + if own_index then break end + end input.loop = { count = 1, - jump_target = input, + jump_target = own_index or 1, runtime_counter = 0, } end @@ -163,12 +168,21 @@ local function controls_for_loop(input, draw, column, top) tooltip = Locales.str("SEMANTIC_WORKFLOW_INPUTS_LOOP_TARGET_TOOL_TIP"), }) then InputListGui.special_select_handler = function(selection) - local input_list = SemanticWorkflowProject.current.sections[selection.section_index].inputs - local own_index = IndexOf(input_list, input) + local sheet = SemanticWorkflowProject:asserted_current() + local current_section_index = nil + local own_index = nil + for s_idx, section in ipairs(sheet.sections) do + own_index = IndexOf(section.inputs, input) + if own_index then + current_section_index = s_idx + break + end + end + if current_section_index ~= selection.section_index then return end if own_index >= selection.input_index then - input.loop.jump_target = input_list[selection.input_index] + input.loop.jump_target = selection.input_index InputListGui.special_select_handler = nil - SemanticWorkflowProject:asserted_current():run_to_preview() + sheet:run_to_preview() end end end diff --git a/src/views/SemanticWorkflow/Implementations/Sheet.lua b/src/views/SemanticWorkflow/Implementations/Sheet.lua index 364be3f..986dff7 100644 --- a/src/views/SemanticWorkflow/Implementations/Sheet.lua +++ b/src/views/SemanticWorkflow/Implementations/Sheet.lua @@ -66,7 +66,8 @@ function __impl:evaluate_frame() or current_action == input.end_action then self._frame_counter = 0 - if input.loop == nil or input.loop.runtime_counter >= input.loop.count then + local loop = input.loop + if loop == nil then self._input_index = self._input_index + 1 if #section.inputs < self._input_index then self.measured_section_lengths[section] = self._section_frame_counter @@ -75,8 +76,23 @@ function __impl:evaluate_frame() self._input_index = 1 end else - input.loop.runtime_counter = input.loop.runtime_counter + 1 - self._input_index = IndexOf(section.inputs, input.loop.jump_target) or 1 + local target_index = loop.jump_target + local runtime_counter = loop.runtime_counter or 0 + if target_index == nil or target_index < 1 or target_index > #section.inputs + or target_index > self._input_index + or (loop.count ~= 0 and runtime_counter >= loop.count) + then + self._input_index = self._input_index + 1 + if #section.inputs < self._input_index then + self.measured_section_lengths[section] = self._section_frame_counter + self._section_frame_counter = 0 + self._section_index = self._section_index + 1 + self._input_index = 1 + end + else + loop.runtime_counter = runtime_counter + 1 + self._input_index = target_index + end end end if self._section_index > self.preview_input.section_index @@ -191,6 +207,15 @@ function __impl:load(file, load_state) end CloneInto(self, contents) + -- ensure loop runtime_counters are initialized after load + for _, section in pairs(self.sections) do + for _, input in pairs(section.inputs) do + if input.loop and input.loop.runtime_counter == nil then + input.loop.runtime_counter = 0 + end + end + end + -- convert sheets pre 2.0.0 if contents.version:match("^%s*[vV]?(%d+)") == '1' then self._frame_counter = self._frame_counter or 0 From d93513af62d60e90dcbcf8bf58685a4c542362eb Mon Sep 17 00:00:00 2001 From: hugou74 Date: Sun, 3 May 2026 17:27:56 +0000 Subject: [PATCH 3/7] chore: add Apache 2.0 license notice for Material Symbols icons FramePerfection requested in review that the loop/loop_light icons (cycle from Google Material Symbols) carry a license notice. Refs: PR #114 review comment --- NOTICE | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..44e1178 --- /dev/null +++ b/NOTICE @@ -0,0 +1,11 @@ +Third-Party Assets +================== + +The following assets are licensed under the Apache License, Version 2.0: + +- src/views/SemanticWorkflow/Resources/loop.png +- src/views/SemanticWorkflow/Resources/loop_light.png + + Source: Google Material Symbols — "cycle" icon + URL: https://fonts.google.com/icons?selected=Material+Symbols+Outlined:cycle + License: Apache-2.0 From 086c8702e70412a8ed4ee8ee227c6ba86a4d96a5 Mon Sep 17 00:00:00 2001 From: hugou74 Date: Sun, 3 May 2026 20:18:41 +0000 Subject: [PATCH 4/7] fix(InputListGui): address Frame's review points #2 and #3 Point 2 (option B): When inserting or deleting inputs, adjust the jump_target indices of looping inputs in the same section. - Insert before => increment targets >= insert_index - Insert after => increment targets >= insert_index - Delete => decrement targets > delete_index, remove loop if target == delete_index Point 3: Only draw the orange target rectangle for the currently selected looping input, instead of all targets at once. This makes it possible to distinguish which loop jumps where. Refs: PR #114 review comments --- .../Implementations/InputListGui.lua | 55 +++++++++++++------ 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/src/views/SemanticWorkflow/Implementations/InputListGui.lua b/src/views/SemanticWorkflow/Implementations/InputListGui.lua index a30f204..cf3093e 100644 --- a/src/views/SemanticWorkflow/Implementations/InputListGui.lua +++ b/src/views/SemanticWorkflow/Implementations/InputListGui.lua @@ -262,24 +262,41 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) end local deferred_calls = { } - local function queue_table_insert(target, reference_item, new_item, offset) - deferred_calls[#deferred_calls+1] = function() - table.insert(target, IndexOf(target, reference_item) + offset, new_item) - -- any_changes = true -- TODO: is this even worth it? + local function adjust_loop_targets_on_insert(section, insert_index) + for _, inp in ipairs(section.inputs) do + if inp.loop and inp.loop.jump_target >= insert_index then + inp.loop.jump_target = inp.loop.jump_target + 1 + end end end - local function queue_table_remove(target, item) - deferred_calls[#deferred_calls+1] = function() - table.remove(target, IndexOf(target, item)) - -- any_changes = true -- TODO: is this even worth it? + + local function adjust_loop_targets_on_delete(section, delete_index) + for _, inp in ipairs(section.inputs) do + if inp.loop then + if inp.loop.jump_target > delete_index then + inp.loop.jump_target = inp.loop.jump_target - 1 + elseif inp.loop.jump_target == delete_index then + inp.loop = nil + end + end end end - local loop_targets = {} - for s_idx, section in ipairs(sheet.sections) do - for _, inp in ipairs(section.inputs) do - if inp.loop and inp.loop.jump_target then - loop_targets[s_idx .. ":" .. inp.loop.jump_target] = true + local function queue_table_insert(target, reference_item, new_item, offset, owning_section) + deferred_calls[#deferred_calls+1] = function() + local insert_index = IndexOf(target, reference_item) + offset + table.insert(target, insert_index, new_item) + if owning_section then + adjust_loop_targets_on_insert(owning_section, insert_index) + end + end + end + local function queue_table_remove(target, item, owning_section) + deferred_calls[#deferred_calls+1] = function() + local delete_index = IndexOf(target, item) + table.remove(target, delete_index) + if owning_section then + adjust_loop_targets_on_delete(owning_section, delete_index) end end end @@ -397,7 +414,7 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) text = '[icon:clone_up]', tooltip = Locales.str("SEMANTIC_WORKFLOW_INPUTS_PREPEND_INPUT_TOOL_TIP") }) then - queue_table_insert(section.inputs, input, ugui.internal.deep_clone(input), 0) + queue_table_insert(section.inputs, input, ugui.internal.deep_clone(input), 0, section) end if ugui.button({ @@ -406,7 +423,7 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) text = '[icon:clone_down]', tooltip = Locales.str("SEMANTIC_WORKFLOW_INPUTS_APPEND_INPUT_TOOL_TIP") }) then - queue_table_insert(section.inputs, input, ugui.internal.deep_clone(input), 1) + queue_table_insert(section.inputs, input, ugui.internal.deep_clone(input), 1, section) end if ugui.button({ @@ -416,7 +433,7 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) tooltip = Locales.str("SEMANTIC_WORKFLOW_INPUTS_DELETE_INPUT_TOOL_TIP"), is_enabled = #section.inputs > 1 }) then - queue_table_remove(section.inputs, input) + queue_table_remove(section.inputs, input, section) end local termination_tool_tip = @@ -499,7 +516,11 @@ local function draw_sections_gui(sheet, draw, section_rect, button_draw_data) BreitbandGraphics.draw_ellipse(rect, input.joy[v.input] and '#000000FF' or '#00000050', 1) end - if loop_targets[section_index .. ":" .. input_index] then + local active = sheet.active_input + if active and active.section_index == section_index + and section.inputs[active.input_index] + and section.inputs[active.input_index].loop + and section.inputs[active.input_index].loop.jump_target == input_index then BreitbandGraphics.draw_rectangle(section_rect, '#FF8000FF', 2) end From 51e199e37660990fde0247a3f1c6de8433d95404 Mon Sep 17 00:00:00 2001 From: hugou74 Date: Sun, 10 May 2026 14:31:46 +0000 Subject: [PATCH 5/7] fix(InputsTab, Sheet): reject nested and interlaced loops FramePerfection requested that nested and interlaced loops be rejected as non-trivial for now. - InputsTab: add is_loop_target_valid() and gate special_select_handler so that selecting a loop target which would create a nested or interlaced loop is silently ignored. - Sheet: add is_loop_valid() and treat invalid loops as non-existent during playback, guarding against corrupted/legacy sheets. Refs: PR #114 review comments --- .../Implementations/InputsTab.lua | 27 +++++++++++++++++++ .../Implementations/Sheet.lua | 23 ++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/views/SemanticWorkflow/Implementations/InputsTab.lua b/src/views/SemanticWorkflow/Implementations/InputsTab.lua index 47b9e4a..124302b 100644 --- a/src/views/SemanticWorkflow/Implementations/InputsTab.lua +++ b/src/views/SemanticWorkflow/Implementations/InputsTab.lua @@ -123,6 +123,30 @@ local function controls_for_end_action(input, draw, column, top) end end +---@param section Section +---@param own_index integer +---@param new_target integer +---@return boolean +local function is_loop_target_valid(section, own_index, new_target) + for other_index, other_input in ipairs(section.inputs) do + if other_index ~= own_index and other_input.loop then + local other_target = other_input.loop.jump_target + if other_target then + local a, b = new_target, own_index + local c, d = other_target, other_index + -- Nested: one strictly inside the other + local nested = (a < c and d < b) or (c < a and b < d) + -- Interlaced: overlapping but neither nested nor touching + local interlaced = (a < c and c < b and b < d) or (c < a and a < d and d < b) + if nested or interlaced then + return false + end + end + end + end + return true +end + ---@param input SectionInputs ---@return boolean any_changes local function controls_for_loop(input, draw, column, top) @@ -170,16 +194,19 @@ local function controls_for_loop(input, draw, column, top) InputListGui.special_select_handler = function(selection) local sheet = SemanticWorkflowProject:asserted_current() local current_section_index = nil + local current_section = nil local own_index = nil for s_idx, section in ipairs(sheet.sections) do own_index = IndexOf(section.inputs, input) if own_index then current_section_index = s_idx + current_section = section break end end if current_section_index ~= selection.section_index then return end if own_index >= selection.input_index then + if not is_loop_target_valid(current_section, own_index, selection.input_index) then return end input.loop.jump_target = selection.input_index InputListGui.special_select_handler = nil sheet:run_to_preview() diff --git a/src/views/SemanticWorkflow/Implementations/Sheet.lua b/src/views/SemanticWorkflow/Implementations/Sheet.lua index 986dff7..beeaf84 100644 --- a/src/views/SemanticWorkflow/Implementations/Sheet.lua +++ b/src/views/SemanticWorkflow/Implementations/Sheet.lua @@ -13,6 +13,28 @@ local __impl = __impl ---@type Section local Section = dofile(views_path .. 'SemanticWorkflow/Definitions/Section.lua') +---@param section Section +---@param own_index integer +---@param new_target integer +---@return boolean +local function is_loop_valid(section, own_index, new_target) + for other_index, other_input in ipairs(section.inputs) do + if other_index ~= own_index and other_input.loop then + local other_target = other_input.loop.jump_target + if other_target then + local a, b = new_target, own_index + local c, d = other_target, other_index + local nested = (a < c and d < b) or (c < a and b < d) + local interlaced = (a < c and c < b and b < d) or (c < a and a < d and d < b) + if nested or interlaced then + return false + end + end + end + end + return true +end + local function playback_speed_mode() return Settings.semantic_workflow.fast_foward and Mupen.CoreSpeedMode.UltraFastForward or Mupen.CoreSpeedMode.Normal end @@ -81,6 +103,7 @@ function __impl:evaluate_frame() if target_index == nil or target_index < 1 or target_index > #section.inputs or target_index > self._input_index or (loop.count ~= 0 and runtime_counter >= loop.count) + or not is_loop_valid(section, self._input_index, target_index) then self._input_index = self._input_index + 1 if #section.inputs < self._input_index then From f1070b780ae1cfa73922b475d2dd1bad0f6c01e1 Mon Sep 17 00:00:00 2001 From: Ramos Hugo <132284948+hugou74130@users.noreply.github.com> Date: Wed, 13 May 2026 18:13:18 +0200 Subject: [PATCH 6/7] Update src/views/SemanticWorkflow/Implementations/InputsTab.lua Co-authored-by: Frame --- .../SemanticWorkflow/Implementations/InputsTab.lua | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/views/SemanticWorkflow/Implementations/InputsTab.lua b/src/views/SemanticWorkflow/Implementations/InputsTab.lua index 124302b..cd07770 100644 --- a/src/views/SemanticWorkflow/Implementations/InputsTab.lua +++ b/src/views/SemanticWorkflow/Implementations/InputsTab.lua @@ -132,14 +132,9 @@ local function is_loop_target_valid(section, own_index, new_target) if other_index ~= own_index and other_input.loop then local other_target = other_input.loop.jump_target if other_target then - local a, b = new_target, own_index - local c, d = other_target, other_index - -- Nested: one strictly inside the other - local nested = (a < c and d < b) or (c < a and b < d) - -- Interlaced: overlapping but neither nested nor touching - local interlaced = (a < c and c < b and b < d) or (c < a and a < d and d < b) - if nested or interlaced then - return false + local overlaps = (new_target <= other_index) and (other_target <= own_index) + if overlaps then + return false end end end From 3d2fe8dcb52276d1440e3e151ce0e5771f77b92e Mon Sep 17 00:00:00 2001 From: hugou74130 Date: Wed, 13 May 2026 18:33:06 +0200 Subject: [PATCH 7/7] fix(loop validation): simplify overlap checks for loop targets --- .../Implementations/InputsTab.lua | 12 ++++----- .../Implementations/Sheet.lua | 25 ++----------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/views/SemanticWorkflow/Implementations/InputsTab.lua b/src/views/SemanticWorkflow/Implementations/InputsTab.lua index 124302b..90aef0e 100644 --- a/src/views/SemanticWorkflow/Implementations/InputsTab.lua +++ b/src/views/SemanticWorkflow/Implementations/InputsTab.lua @@ -1,3 +1,4 @@ + -- -- Copyright (c) 2025, Mupen64 maintainers. -- @@ -132,13 +133,9 @@ local function is_loop_target_valid(section, own_index, new_target) if other_index ~= own_index and other_input.loop then local other_target = other_input.loop.jump_target if other_target then - local a, b = new_target, own_index - local c, d = other_target, other_index - -- Nested: one strictly inside the other - local nested = (a < c and d < b) or (c < a and b < d) - -- Interlaced: overlapping but neither nested nor touching - local interlaced = (a < c and c < b and b < d) or (c < a and a < d and d < b) - if nested or interlaced then + -- Overlap check covers nested, interlaced, and more edge cases + local overlaps = (new_target <= other_index) and (other_target <= own_index) + if overlaps then return false end end @@ -576,3 +573,4 @@ function __impl.render(draw) draw_funcs[InputListGui.view_index](draw, edited_input) end end + diff --git a/src/views/SemanticWorkflow/Implementations/Sheet.lua b/src/views/SemanticWorkflow/Implementations/Sheet.lua index beeaf84..8217f7c 100644 --- a/src/views/SemanticWorkflow/Implementations/Sheet.lua +++ b/src/views/SemanticWorkflow/Implementations/Sheet.lua @@ -1,3 +1,4 @@ + -- -- Copyright (c) 2025, Mupen64 maintainers. -- @@ -13,28 +14,6 @@ local __impl = __impl ---@type Section local Section = dofile(views_path .. 'SemanticWorkflow/Definitions/Section.lua') ----@param section Section ----@param own_index integer ----@param new_target integer ----@return boolean -local function is_loop_valid(section, own_index, new_target) - for other_index, other_input in ipairs(section.inputs) do - if other_index ~= own_index and other_input.loop then - local other_target = other_input.loop.jump_target - if other_target then - local a, b = new_target, own_index - local c, d = other_target, other_index - local nested = (a < c and d < b) or (c < a and b < d) - local interlaced = (a < c and c < b and b < d) or (c < a and a < d and d < b) - if nested or interlaced then - return false - end - end - end - end - return true -end - local function playback_speed_mode() return Settings.semantic_workflow.fast_foward and Mupen.CoreSpeedMode.UltraFastForward or Mupen.CoreSpeedMode.Normal end @@ -103,7 +82,6 @@ function __impl:evaluate_frame() if target_index == nil or target_index < 1 or target_index > #section.inputs or target_index > self._input_index or (loop.count ~= 0 and runtime_counter >= loop.count) - or not is_loop_valid(section, self._input_index, target_index) then self._input_index = self._input_index + 1 if #section.inputs < self._input_index then @@ -282,3 +260,4 @@ function __impl:set_base_sheet(sheet) self._base_sheet = sheet self._savestate = nil end +