Skip to content
Open
11 changes: 11 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions src/core/Locales.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/res/lang/en_US.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions src/res/lang/fr_FR.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 :',
Expand Down
15 changes: 15 additions & 0 deletions src/views/SemanticWorkflow/Definitions/SectionInputs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,25 @@
-- SPDX-License-Identifier: GPL-2.0-or-later
--

---@class Loop Runtime information for a section loop.
---@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.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally made this field a SectionInputs because that way the loop target correctly move along when inputs are added before it.
This functionality should be restored, by either:

  • making this a SectionInputs again and only use indexing logic for the save/load routines, and handling deletion of the loop target explicitly
  • changing the target indices of looping inputs when inserting and deleting inputs before their respective targets

I'd personally probably actually go for the second option, now that I think about it.

---@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
56 changes: 47 additions & 9 deletions src/views/SemanticWorkflow/Implementations/InputListGui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -262,16 +262,42 @@ 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)
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 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 function queue_table_insert(target, reference_item, new_item, offset, owning_section)
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 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)
local function queue_table_remove(target, item, owning_section)
deferred_calls[#deferred_calls+1] = function()
table.remove(target, IndexOf(target, item))
-- any_changes = true -- TODO: is this even worth it?
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
Comment thread
hugou74130 marked this conversation as resolved.

Expand Down Expand Up @@ -388,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({
Expand All @@ -397,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({
Expand All @@ -407,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 =
Expand Down Expand Up @@ -490,6 +516,18 @@ 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

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

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
Expand Down
101 changes: 100 additions & 1 deletion src/views/SemanticWorkflow/Implementations/InputsTab.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

--
-- Copyright (c) 2025, Mupen64 maintainers.
--
Expand Down Expand Up @@ -65,6 +66,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)

Expand Down Expand Up @@ -120,6 +124,97 @@ 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
-- 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
end
end
return true
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
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 = own_index or 1,
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 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()
end
end
end
end

return any_changes
end

local function section_controls_for_selected(draw, edited_input)
local sheet = SemanticWorkflowProject:asserted_current()

Expand All @@ -145,6 +240,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
Expand Down Expand Up @@ -244,7 +343,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
Expand Down Expand Up @@ -475,3 +573,4 @@ function __impl.render(draw)
draw_funcs[InputListGui.view_index](draw, edited_input)
end
end

5 changes: 4 additions & 1 deletion src/views/SemanticWorkflow/Implementations/Section.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
22 changes: 22 additions & 0 deletions src/views/SemanticWorkflow/Implementations/SectionInputs.lua
Original file line number Diff line number Diff line change
@@ -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
Loading