-- name: Character Poser -- description: pose your character, save your poses or something. mod created by Yam local mod_version = "1.1" local save_version = "2" local legacy_save_version = "1" local storage_prefix = "character_poser_" local default_pose_capacity = 30 local maximum_pose_capacity = 300 local maximum_pose_name_length = 32 local minimum_character_height = -1000 local maximum_character_height = 1000 local text_color = "\\#ffffff\\" local axes = { "x", "y", "z" } local hand_names = { [0] = "fists", [1] = "open", [2] = "peace sign", [3] = "holding cap", [4] = "holding wing cap", [5] = "right hand open", } local joints = { { name = "body", part = MARIO_ANIM_PART_ROOT }, { name = "torso", part = MARIO_ANIM_PART_BUTT }, { name = "head", part = MARIO_ANIM_PART_HEAD }, { name = "left shoulder", part = MARIO_ANIM_PART_UPPER_LEFT }, { name = "left elbow", part = MARIO_ANIM_PART_LEFT_ARM }, { name = "left wrist", part = MARIO_ANIM_PART_LEFT_FOREARM }, { name = "right shoulder", part = MARIO_ANIM_PART_UPPER_RIGHT }, { name = "right elbow", part = MARIO_ANIM_PART_RIGHT_ARM }, { name = "right wrist", part = MARIO_ANIM_PART_RIGHT_FOREARM }, { name = "left hip", part = MARIO_ANIM_PART_LOWER_LEFT }, { name = "left knee", part = MARIO_ANIM_PART_LEFT_THIGH }, { name = "left ankle", part = MARIO_ANIM_PART_LEFT_LEG }, { name = "right hip", part = MARIO_ANIM_PART_LOWER_RIGHT }, { name = "right knee", part = MARIO_ANIM_PART_RIGHT_THIGH }, { name = "right ankle", part = MARIO_ANIM_PART_RIGHT_LEG }, } local joint_for_part = {} for i = 1, #joints do joint_for_part[joints[i].part] = i end local function clamp(value, minimum, maximum) if value < minimum then return minimum end if value > maximum then return maximum end return value end local function round(value) if value >= 0 then return math.floor(value + 0.5) end return math.ceil(value - 0.5) end local function default_pose_name(slot) return "Pose " .. tostring(slot) end local function load_storage(key) local value = mod_storage_load(storage_prefix .. key) if value == nil or value == "" then return mod_storage_load(key) end return value end local function save_storage(key, value) mod_storage_save(storage_prefix .. key, value) end local function normalize_pose_name(name, slot) if type(name) ~= "string" then return default_pose_name(slot) end name = string.gsub(name, "[%c]", " ") name = string.gsub(name, "\\", "") name = string.gsub(name, "%s+", " ") name = string.match(name, "^%s*(.-)%s*$") or "" name = string.sub(name, 1, maximum_pose_name_length) if name == "" then return default_pose_name(slot) end return name end local function get_pose_name(slot) return normalize_pose_name(load_storage("saved_pose_name_" .. slot), slot) end local function info_message(message) djui_chat_message_create(text_color .. "Character Poser: " .. message) end local function error_message(message) djui_chat_message_create(text_color .. "Character Poser: " .. message) end local function new_pose() local pose = {} for i = 1, #joints do pose[i] = { rot = { x = 0, y = 0, z = 0 }, pos = { x = 0, y = 0, z = 0 }, } end return pose end local function encode_channel(pose, channel) local values = {} for i = 1, #joints do for _, axis in ipairs(axes) do values[#values + 1] = tostring(round(pose[i][channel][axis])) end end return table.concat(values, ",") end local function decode_numbers(raw, minimum, maximum) if type(raw) ~= "string" then return nil end local expected = #joints * #axes local numbers = {} for token in string.gmatch(raw, "[^,]+") do local value = tonumber(token) if value == nil then return nil end numbers[#numbers + 1] = clamp(round(value), minimum, maximum) if #numbers > expected then return nil end end if #numbers ~= expected then return nil end return numbers end local function apply_numbers(pose, channel, numbers) local cursor = 1 for i = 1, #joints do for _, axis in ipairs(axes) do pose[i][channel][axis] = numbers[cursor] cursor = cursor + 1 end end end local function decode_pose(rot_raw, pos_raw) local rotations = decode_numbers(rot_raw, -180, 180) local positions = decode_numbers(pos_raw, -100, 100) if rotations == nil or positions == nil then return nil end local pose = new_pose() apply_numbers(pose, "rot", rotations) apply_numbers(pose, "pos", positions) return pose end local local_pose = new_pose() local local_hand = MARIO_HAND_FISTS local local_apose = true local local_character_height = 0 local selected_joint = 1 local selected_axis = 1 local pose_capacity = clamp(round(tonumber(load_storage("pose_capacity")) or default_pose_capacity), default_pose_capacity, maximum_pose_capacity) local selected_slot = clamp(round(tonumber(load_storage("selected_pose_slot")) or 1), 1, pose_capacity) local selected_pose_name = get_pose_name(selected_slot) local editor_open = false local storage_dirty = false local storage_timer = 0 local menu_refreshing = false local menu = {} local zero_pose = new_pose() local zero_rot = encode_channel(zero_pose, "rot") local zero_pos = encode_channel(zero_pose, "pos") local function encode_saved_pose() return table.concat({ save_version, tostring(local_hand), local_apose and "1" or "0", tostring(round(local_character_height)), encode_channel(local_pose, "rot"), encode_channel(local_pose, "pos"), }, "|") end local function decode_saved_pose(raw) if type(raw) ~= "string" then return false end local version, hand, apose, height, rotations, positions = string.match(raw, "^([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)$") if version ~= save_version then version, hand, apose, rotations, positions = string.match(raw, "^([^|]*)|([^|]*)|([^|]*)|([^|]*)|([^|]*)$") if version ~= legacy_save_version then return false end height = "0" end local decoded = decode_pose(rotations, positions) local hand_number = tonumber(hand) local height_number = tonumber(height) if decoded == nil or hand_number == nil or height_number == nil then return false end local_pose = decoded local_hand = clamp(round(hand_number), MARIO_HAND_FISTS, MARIO_HAND_RIGHT_OPEN) local_apose = apose == "1" local_character_height = clamp(round(height_number), minimum_character_height, maximum_character_height) return true end decode_saved_pose(load_storage("autosave")) for i = 0, MAX_PLAYERS - 1 do local sync = gPlayerSyncTable[i] if sync.characterPoserEnabled == nil then sync.characterPoserEnabled = false end if sync.characterPoserRot == nil then sync.characterPoserRot = zero_rot end if sync.characterPoserPos == nil then sync.characterPoserPos = zero_pos end if sync.characterPoserHand == nil then sync.characterPoserHand = MARIO_HAND_FISTS end if sync.characterPoserApose == nil then sync.characterPoserApose = true end if sync.characterPoserHeight == nil then sync.characterPoserHeight = 0 end end gPlayerSyncTable[0].characterPoserRot = encode_channel(local_pose, "rot") gPlayerSyncTable[0].characterPoserPos = encode_channel(local_pose, "pos") gPlayerSyncTable[0].characterPoserHand = local_hand gPlayerSyncTable[0].characterPoserApose = local_apose gPlayerSyncTable[0].characterPoserHeight = local_character_height gPlayerSyncTable[0].characterPoserEnabled = false local function select_pose_slot(slot) selected_slot = clamp(round(slot), 1, pose_capacity) selected_pose_name = get_pose_name(selected_slot) save_storage("selected_pose_slot", tostring(selected_slot)) end local function refresh_menu() menu_refreshing = true if menu.enabled ~= nil then update_mod_menu_element_checkbox(menu.enabled, gPlayerSyncTable[0].characterPoserEnabled) end if menu.apose ~= nil then update_mod_menu_element_checkbox(menu.apose, local_apose) end if menu.joint ~= nil then update_mod_menu_element_name(menu.joint, string.format("Joint: %s", joints[selected_joint].name)) update_mod_menu_element_slider(menu.joint, selected_joint) end local transform = local_pose[selected_joint] for _, axis in ipairs(axes) do if menu["rot_" .. axis] ~= nil then update_mod_menu_element_name(menu["rot_" .. axis], string.format("Rotation %s: %d deg", string.upper(axis), transform.rot[axis])) update_mod_menu_element_slider(menu["rot_" .. axis], transform.rot[axis]) end if menu["pos_" .. axis] ~= nil then update_mod_menu_element_name(menu["pos_" .. axis], string.format("Position %s: %d", string.upper(axis), transform.pos[axis])) update_mod_menu_element_slider(menu["pos_" .. axis], transform.pos[axis]) end end if menu.hand ~= nil then update_mod_menu_element_name(menu.hand, "Hands: " .. hand_names[local_hand]) update_mod_menu_element_slider(menu.hand, local_hand) end if menu.height ~= nil then update_mod_menu_element_name(menu.height, string.format("Character height: %+d", local_character_height)) update_mod_menu_element_slider(menu.height, local_character_height) end if menu.capacity ~= nil then update_mod_menu_element_name(menu.capacity, "Pose capacity: " .. pose_capacity) update_mod_menu_element_slider(menu.capacity, pose_capacity) end if menu.slot ~= nil then update_mod_menu_element_name(menu.slot, string.format("Pose slot: %d/%d - %s", selected_slot, pose_capacity, selected_pose_name)) update_mod_menu_element_slider(menu.slot, selected_slot) end if menu.pose_name ~= nil then update_mod_menu_element_inputbox(menu.pose_name, selected_pose_name) end menu_refreshing = false end local function publish_pose() gPlayerSyncTable[0].characterPoserRot = encode_channel(local_pose, "rot") gPlayerSyncTable[0].characterPoserPos = encode_channel(local_pose, "pos") gPlayerSyncTable[0].characterPoserHand = local_hand gPlayerSyncTable[0].characterPoserApose = local_apose gPlayerSyncTable[0].characterPoserHeight = local_character_height storage_dirty = true storage_timer = 0 refresh_menu() end local function set_enabled(enabled) gPlayerSyncTable[0].characterPoserEnabled = enabled if not enabled then editor_open = false end refresh_menu() end local function open_editor() set_enabled(true) editor_open = true info_message("live editor opened") end local function reset_joint(index) local_pose[index] = new_pose()[1] publish_pose() end local function reset_all() local_pose = new_pose() local_character_height = 0 publish_pose() end local function save_autosave() save_storage("autosave", encode_saved_pose()) storage_dirty = false storage_timer = 0 end local function save_pose(slot) slot = clamp(round(slot), 1, pose_capacity) if slot ~= selected_slot then select_pose_slot(slot) end selected_pose_name = normalize_pose_name(selected_pose_name, selected_slot) save_storage("saved_pose_" .. selected_slot, encode_saved_pose()) save_storage("saved_pose_name_" .. selected_slot, selected_pose_name) save_autosave() refresh_menu() info_message(string.format("saved '%s' in slot %d", selected_pose_name, selected_slot)) end local function load_pose(slot) slot = clamp(round(slot), 1, pose_capacity) select_pose_slot(slot) local raw = load_storage("saved_pose_" .. selected_slot) if type(raw) ~= "string" or raw == "" then raw = load_storage("preset_" .. selected_slot) end if not decode_saved_pose(raw) then refresh_menu() error_message("pose slot " .. selected_slot .. " is empty") return false end publish_pose() save_autosave() info_message(string.format("loaded '%s' from slot %d", selected_pose_name, selected_slot)) return true end local function rename_selected_pose(name) selected_pose_name = normalize_pose_name(name, selected_slot) save_storage("saved_pose_name_" .. selected_slot, selected_pose_name) refresh_menu() end local function set_pose_capacity(value) pose_capacity = clamp(round(value), default_pose_capacity, maximum_pose_capacity) save_storage("pose_capacity", tostring(pose_capacity)) if selected_slot > pose_capacity then select_pose_slot(pose_capacity) end refresh_menu() end local pose_cache = {} local function degrees_to_sm64(degrees) return round(degrees * 65536 / 360) end local function get_cached_pose(player_index) local sync = gPlayerSyncTable[player_index] local rot_raw = sync.characterPoserRot or zero_rot local pos_raw = sync.characterPoserPos or zero_pos local cached = pose_cache[player_index] if cached ~= nil and cached.rot_raw == rot_raw and cached.pos_raw == pos_raw then return cached.transforms end local decoded = decode_pose(rot_raw, pos_raw) or new_pose() local transforms = {} for i = 1, #joints do local source = decoded[i] local transform = { rot = { x = degrees_to_sm64(source.rot.x), y = degrees_to_sm64(source.rot.y), z = degrees_to_sm64(source.rot.z), }, pos = { x = source.pos.x, y = source.pos.y, z = source.pos.z, }, } transform.identity = source.rot.x == 0 and source.rot.y == 0 and source.rot.z == 0 and source.pos.x == 0 and source.pos.y == 0 and source.pos.z == 0 transforms[i] = transform end pose_cache[player_index] = { rot_raw = rot_raw, pos_raw = pos_raw, transforms = transforms, } return transforms end local hooked_roots = {} local function mark_pose_nodes(first, visited) if first == nil then return end local node = first repeat if node == nil or visited[node] then break end visited[node] = true if node.type == GRAPH_NODE_TYPE_ANIMATED_PART or node.type == GRAPH_NODE_TYPE_BONE then node.hookProcess = 1 end if node.children ~= nil then mark_pose_nodes(node.children, visited) end node = node.next until node == nil or node == first end local function ensure_model_is_hooked(m) if m.marioObj == nil then return end local root = m.marioObj.header.gfx.sharedChild if root == nil or hooked_roots[root] then return end mark_pose_nodes(root, {}) hooked_roots[root] = true end local function on_geo_process_children(_, mat_stack_index) if geo_get_mario_object() == nil then return end local m = geo_get_mario_state() if m == nil then return end local sync = gPlayerSyncTable[m.playerIndex] if sync == nil or not sync.characterPoserEnabled then return end local part = m.marioBodyState.currAnimPart local joint_index = joint_for_part[part] if joint_index == nil then return end local transform = get_cached_pose(m.playerIndex)[joint_index] if transform == nil then return end local character_height = 0 if joint_index == joint_for_part[MARIO_ANIM_PART_ROOT] then character_height = clamp(tonumber(sync.characterPoserHeight) or 0, minimum_character_height, maximum_character_height) end if transform.identity and character_height == 0 then return end if mat_stack_index < 0 or mat_stack_index >= 63 then return end local scratch = gMatStack[mat_stack_index + 1] local position = transform.pos if character_height ~= 0 then position = { x = transform.pos.x, y = transform.pos.y + character_height, z = transform.pos.z, } end mtxf_rotate_xyz_and_translate(scratch, position, transform.rot) mtxf_mul(gMatStack[mat_stack_index], scratch, gMatStack[mat_stack_index]) mtxf_mul(gMatStackPrev[mat_stack_index], scratch, gMatStackPrev[mat_stack_index]) end local function mario_update(m) ensure_model_is_hooked(m) local sync = gPlayerSyncTable[m.playerIndex] if sync == nil or not sync.characterPoserEnabled then return end m.marioBodyState.handState = clamp( tonumber(sync.characterPoserHand) or MARIO_HAND_FISTS, MARIO_HAND_FISTS, MARIO_HAND_RIGHT_OPEN ) if sync.characterPoserApose then set_character_animation(m, CHAR_ANIM_A_POSE) set_anim_to_frame(m, 0) end end local repeat_ticks = {} local function pressed_or_repeated(button, pressed, down) if down & button == 0 then repeat_ticks[button] = 0 return false end local ticks = (repeat_ticks[button] or 0) + 1 repeat_ticks[button] = ticks return pressed & button ~= 0 or (ticks >= 14 and ticks % 3 == 0) end local function edit_controls(m) if m.playerIndex ~= 0 or not editor_open then return end local controller = m.controller local pressed = controller.buttonPressed local down = controller.buttonDown local edit_mask = A_BUTTON | B_BUTTON | L_TRIG | R_TRIG | Z_TRIG | START_BUTTON | U_JPAD | D_JPAD | L_JPAD | R_JPAD | U_CBUTTONS | D_CBUTTONS if pressed & START_BUTTON ~= 0 then editor_open = false save_autosave() info_message("editor closed; pose remains active") elseif down & Z_TRIG ~= 0 and pressed & B_BUTTON ~= 0 then reset_all() info_message("reset the full pose") elseif pressed & B_BUTTON ~= 0 then reset_joint(selected_joint) elseif pressed & A_BUTTON ~= 0 then local_hand = (local_hand + 1) % (MARIO_HAND_RIGHT_OPEN + 1) publish_pose() end if pressed_or_repeated(L_TRIG, pressed, down) then selected_joint = selected_joint - 1 if selected_joint < 1 then selected_joint = #joints end refresh_menu() elseif pressed_or_repeated(R_TRIG, pressed, down) then selected_joint = selected_joint + 1 if selected_joint > #joints then selected_joint = 1 end refresh_menu() end if pressed_or_repeated(U_JPAD, pressed, down) then selected_axis = selected_axis - 1 if selected_axis < 1 then selected_axis = #axes end elseif pressed_or_repeated(D_JPAD, pressed, down) then selected_axis = selected_axis + 1 if selected_axis > #axes then selected_axis = 1 end end local direction = 0 if pressed_or_repeated(L_JPAD, pressed, down) then direction = -1 end if pressed_or_repeated(R_JPAD, pressed, down) then direction = 1 end if direction ~= 0 then local axis = axes[selected_axis] local transform = local_pose[selected_joint] if down & Z_TRIG ~= 0 then transform.pos[axis] = clamp(transform.pos[axis] + direction * 2, -100, 100) else transform.rot[axis] = clamp(transform.rot[axis] + direction * 5, -180, 180) end publish_pose() end local height_direction = 0 if pressed_or_repeated(U_CBUTTONS, pressed, down) then height_direction = 1 end if pressed_or_repeated(D_CBUTTONS, pressed, down) then height_direction = -1 end if height_direction ~= 0 then local_character_height = clamp(local_character_height + height_direction * 10, minimum_character_height, maximum_character_height) publish_pose() end controller.buttonPressed = pressed & ~edit_mask controller.buttonDown = down & ~edit_mask controller.buttonReleased = controller.buttonReleased & ~edit_mask controller.stickX = 0 controller.stickY = 0 controller.stickMag = 0 controller.rawStickX = 0 controller.rawStickY = 0 end local function draw_text(text, x, y, scale) djui_hud_set_color(0, 0, 0, 255) djui_hud_print_text(text, x - 1, y, scale) djui_hud_print_text(text, x + 1, y, scale) djui_hud_print_text(text, x, y - 1, scale) djui_hud_print_text(text, x, y + 1, scale) djui_hud_set_color(255, 255, 255, 255) djui_hud_print_text(text, x, y, scale) end local function hud_render() if not editor_open then return end djui_hud_set_resolution(RESOLUTION_N64) djui_hud_set_filter(FILTER_NEAREST) djui_hud_set_font(FONT_RECOLOR_HUD) local x = 10 local y = 22 local scale = 0.35 local transform = local_pose[selected_joint] draw_text("CHARACTER POSER", x + 7, y + 7, 0.45) draw_text(string.format("joint %02d/%02d %s", selected_joint, #joints, joints[selected_joint].name), x + 7, y + 22, scale) draw_text(" axis rotation move", x + 7, y + 35, scale) for i, axis in ipairs(axes) do local row_y = y + 46 + (i - 1) * 13 local marker = selected_axis == i and ">" or " " draw_text(string.format("%s %s %+4d deg %+4d", marker, string.upper(axis), transform.rot[axis], transform.pos[axis]), x + 8, row_y, scale) end local base_name = local_apose and "a-pose" or "animation" draw_text("hands: " .. hand_names[local_hand] .. " | base: " .. base_name, x + 7, y + 87, scale) draw_text(string.format("character height: %+d", local_character_height), x + 7, y + 101, scale) draw_text("c-up/down height | d-pad up/down axis", x + 7, y + 114, 0.3) draw_text("hold z + left/right move | l/r joint", x + 7, y + 125, 0.3) draw_text("a hands | b reset joint | start done", x + 7, y + 136, 0.3) end local function update_storage() if not storage_dirty then return end storage_timer = storage_timer + 1 if storage_timer >= 30 then save_autosave() end end local function split_words(message) local words = {} for word in string.gmatch(message or "", "%S+") do words[#words + 1] = word end return words end local function find_joint(words, first) local number = tonumber(words[first] or "") if number ~= nil then number = round(number) if number >= 1 and number <= #joints then return number end end local parts = {} for i = first, #words do parts[#parts + 1] = string.lower(words[i]) end local name = table.concat(parts, " "):gsub("[_%-]", " ") for i = 1, #joints do if joints[i].name == name then return i end end return nil end local function show_help() djui_chat_message_create(text_color .. "Character Poser " .. mod_version .. " commands:") djui_chat_message_create("/pose - open/close live editor | /pose on | /pose off") djui_chat_message_create("/pose joint | /pose set <-180..180>") djui_chat_message_create("/pose move <-100..100> | /pose hand <0-5/name>") djui_chat_message_create("/pose height <-1000..1000> | /pose base ") djui_chat_message_create("/pose reset | /pose name ") djui_chat_message_create(string.format("/pose save <1-%d> [name] | /pose load <1-%d>", pose_capacity, pose_capacity)) djui_chat_message_create(string.format("/pose capacity <%d-%d>", default_pose_capacity, maximum_pose_capacity)) end local function pose_command(message) local words = split_words(message) local command = string.lower(words[1] or "") if command == "" or command == "edit" then if editor_open then editor_open = false save_autosave() info_message("editor closed; pose remains active") else open_editor() end return true elseif command == "on" then set_enabled(true) info_message("pose enabled") return true elseif command == "off" then set_enabled(false) save_autosave() info_message("pose disabled") return true elseif command == "help" then show_help() return true elseif command == "joint" then local index = find_joint(words, 2) if index == nil then error_message("unknown joint") return true end selected_joint = index refresh_menu() info_message("editing " .. joints[index].name) return true elseif command == "set" or command == "move" then local axis = string.lower(words[2] or "") local value = tonumber(words[3] or "") if (axis ~= "x" and axis ~= "y" and axis ~= "z") or value == nil then error_message("use /pose " .. command .. " ") return true end local channel = command == "set" and "rot" or "pos" local minimum = command == "set" and -180 or -100 local maximum = command == "set" and 180 or 100 local_pose[selected_joint][channel][axis] = clamp(round(value), minimum, maximum) publish_pose() return true elseif command == "height" then local value = tonumber(words[2] or "") if value == nil then error_message(string.format("height must be %d to %d", minimum_character_height, maximum_character_height)) return true end local_character_height = clamp(round(value), minimum_character_height, maximum_character_height) publish_pose() return true elseif command == "hand" then local argument = string.lower(table.concat(words, " ", 2)):gsub("[_%-]", " ") local value = tonumber(argument) if value == nil then for hand, name in pairs(hand_names) do if name == argument then value = hand break end end end if value == nil then error_message("hand must be 0-5 or a hand name") return true end local_hand = clamp(round(value), MARIO_HAND_FISTS, MARIO_HAND_RIGHT_OPEN) publish_pose() return true elseif command == "base" or command == "freeze" then local argument = string.lower(words[2] or "") if argument == "animation" or argument == "animated" or argument == "off" then local_apose = false elseif argument == "apose" or argument == "a-pose" or argument == "a_pose" or argument == "on" then local_apose = true else local_apose = not local_apose end publish_pose() return true elseif command == "reset" then if string.lower(words[2] or "") == "all" then reset_all() else reset_joint(selected_joint) end return true elseif command == "name" then local name = table.concat(words, " ", 2) if name == "" then error_message("use /pose name ") return true end rename_selected_pose(name) info_message(string.format("slot %d renamed to '%s'", selected_slot, selected_pose_name)) return true elseif command == "capacity" then local value = tonumber(words[2] or "") if value == nil or value < default_pose_capacity or value > maximum_pose_capacity then error_message(string.format("pose capacity must be %d to %d", default_pose_capacity, maximum_pose_capacity)) return true end set_pose_capacity(value) info_message("pose capacity set to " .. pose_capacity) return true elseif command == "save" or command == "load" then local slot = tonumber(words[2] or "") if slot == nil or slot < 1 or slot > pose_capacity then error_message(string.format("pose slot must be 1-%d", pose_capacity)) return true end select_pose_slot(round(slot)) if command == "save" then if words[3] ~= nil then selected_pose_name = normalize_pose_name(table.concat(words, " ", 3), selected_slot) end save_pose(selected_slot) else load_pose(selected_slot) end return true end show_help() return true end hook_mod_menu_text("Character Poser " .. mod_version) menu.enabled = hook_mod_menu_checkbox("Pose enabled", false, function(_, value) if menu_refreshing then return end set_enabled(value) end) menu.open = hook_mod_menu_button("Open live editor", function() open_editor() end) menu.apose = hook_mod_menu_checkbox("Use A-pose base", local_apose, function(_, value) if menu_refreshing then return end local_apose = value publish_pose() end) menu.height = hook_mod_menu_slider("Character height", local_character_height, minimum_character_height, maximum_character_height, function(_, value) if menu_refreshing then return end local_character_height = clamp(round(value), minimum_character_height, maximum_character_height) publish_pose() end) menu.joint = hook_mod_menu_slider("Joint", selected_joint, 1, #joints, function(_, value) if menu_refreshing then return end selected_joint = clamp(round(value), 1, #joints) refresh_menu() end) for _, axis in ipairs(axes) do local selected_axis_name = axis menu["rot_" .. axis] = hook_mod_menu_slider("Rotation " .. string.upper(axis), 0, -180, 180, function(_, value) if menu_refreshing then return end local_pose[selected_joint].rot[selected_axis_name] = clamp(round(value), -180, 180) publish_pose() end) end for _, axis in ipairs(axes) do local selected_axis_name = axis menu["pos_" .. axis] = hook_mod_menu_slider("Position " .. string.upper(axis), 0, -100, 100, function(_, value) if menu_refreshing then return end local_pose[selected_joint].pos[selected_axis_name] = clamp(round(value), -100, 100) publish_pose() end) end menu.hand = hook_mod_menu_slider("Hands", local_hand, MARIO_HAND_FISTS, MARIO_HAND_RIGHT_OPEN, function(_, value) if menu_refreshing then return end local_hand = clamp(round(value), MARIO_HAND_FISTS, MARIO_HAND_RIGHT_OPEN) publish_pose() end) menu.reset_joint = hook_mod_menu_button("Reset selected joint", function() reset_joint(selected_joint) end) menu.reset_all = hook_mod_menu_button("Reset full pose", reset_all) hook_mod_menu_text("Saved poses") menu.capacity = hook_mod_menu_slider("Pose capacity", pose_capacity, default_pose_capacity, maximum_pose_capacity, function(_, value) if menu_refreshing then return end set_pose_capacity(value) end) menu.slot = hook_mod_menu_slider("Pose slot", selected_slot, 1, maximum_pose_capacity, function(_, value) if menu_refreshing then return end select_pose_slot(value) refresh_menu() end) menu.pose_name = hook_mod_menu_inputbox("Pose name", selected_pose_name, maximum_pose_name_length + 1, function(_, value) if menu_refreshing then return end rename_selected_pose(value) end) menu.save = hook_mod_menu_button("Save selected pose", function() save_pose(selected_slot) end) menu.load = hook_mod_menu_button("Load selected pose", function() load_pose(selected_slot) end) refresh_menu() hook_chat_command("pose", "[edit|on|off|joint|set|move|height|hand|base|reset|name|capacity|save|load|help] - edit your character pose", pose_command) hook_event(HOOK_BEFORE_MARIO_UPDATE, edit_controls) hook_event(HOOK_MARIO_UPDATE, mario_update) hook_event(HOOK_ON_GEO_PROCESS_CHILDREN, on_geo_process_children) hook_event(HOOK_ON_HUD_RENDER, hud_render) hook_event(HOOK_UPDATE, update_storage) hook_event(HOOK_ON_EXIT, function() if storage_dirty then save_autosave() end end) if HOOK_ON_DYNOS_PACK_TOGGLED ~= nil then hook_event(HOOK_ON_DYNOS_PACK_TOGGLED, function() hooked_roots = {} end) end