My first Lua script. Enjoy!
-------------------------------------------
-- MARIO PARTY 8 RNG MANIPULATION HELPER --
-------------------------------------------
-- Author: mohoc.
-- Created on 27/08/2024.
-- Last modified on 09/09/2024.
-- You are free to use and modify this code. Enjoy!
---------------------------------------------------
-- Requirements:
-- 1. Use Lua Core v4.3 by MikeXander
---- https://github.com/MikeXander/Dolphin-Lua-Core/releases/tag/v4.3
-- 2. No GameCube controllers and a single Wiimote.
---------------------------------------------------
------------------
-- INSTRUCTIONS --
------------------
-- Getting started
-- 1. Put this file in the "Sys > Scripts" folder of the emulator.
-- 2. On the emulator: go to "Tools > Execute Script" and select this script.
-- Preparation
-- 1. Use the main global variables below to define your sequence of actions
-- and the range of authorized delay values for each action.
---- See the "ACTIONS MACRO" section for a list of already defined actions.
-- 2. Make a save state a couple frames before when the first action must happen.
-- Running the script
-- 1. Press "Start" in the "Lauch Script" window ("Tools > Execute Script").
-- 2. Load your save state.
-- 2.b. (Optional:) frame advance once you load your save state.
---- This will display the results of the sanity checks.
---- If "Getting started." is displayed then it should be good.
---- Otherwise you will get a warning.
-- 3. Unpause the emulator to play the first sequence.
---- You may pause, make save states or enter extra inputs manually at will.
-- 4. Reload your save state to play the next sequence.
---- Sequences are considered in lexicographic order of their delay sequences.
---- You must go past the first action. Otherwise, the sequence will not be updated.
-- 5. Once the last sequence is played, loading your save state will end the script.
---------------------------
-- MAIN GLOBAL VARIABLES --
---------------------------
-- To be modified.
-- Note: consistently selecting menu options requires you to
-- point at it for at least 3 frames and then press A.
seqActions = {"A"}
seqFrames = {90}
seqDelayUpperBounds = {10}
seqDelayLowerBounds = {0}
maxTotalDelay = 10
minTotalDelay = 0
--[[
-- Example 1: DK board, pre-turn 1
-- seqActions = {"+", "A", "A", "pNoLow", "pNoLow", "pNoLow", "A", "A", "shake", "shake"}
-- seqFrames = {5332, 5394, 5405, 5442, 5443, 5444, 5445, 5486, 5527, 5528}
-- seqDelayUpperBounds = {3, 1, 1, 1, 0, 0, 0, 3, 0, 0}
-- seqDelayLowerBounds = {3, 0, 0, 0, 0, 0, 0, 3, 0, 0}
--]]
--[[
-- Example 2: Shy Guy board, turn 1
seqActions = {"A", "shake",
"pNoLow", "pNoLow", "pNoLow", "A",
"A", "shake"}
seqFrames = {25476, 25519,
25661, 25662, 25663, 25664,
25773, 25827}
seqDelayUpperBounds = {0, 1,
0, 0, 0, 0,
8, 8}
seqDelayLowerBounds = {0, 0,
0, 0, 0, 0,
0, 0}
--]]
--[[
-- Example 3: Bowser board, turn 2 (from confirming the Bowser candy)
seqActions = {"pYesCandy", "pYesCandy", "pYesCandy", "A", "shake", "shake"}
seqFrames = {42282, 42283, 42284, 42285, 42572, 42627}
seqDelayUpperBounds = {10, 0, 0, 0, 10, 0}
seqDelayLowerBounds = {0, 0, 0, 0, 0, 0}
--]]
---------------------
-- TABLE FUNCTIONS --
---------------------
function tableLength(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
function printTable(T)
local toPrint = "["
for pos, _ in ipairs(T) do
toPrint = toPrint .. T[pos] .. ", "
end
toPrint = toPrint .. "]"
SetScreenText(toPrint)
end
function inTable(T, x)
for _, v in ipairs(T) do
if v == x then
return true
end
end
return false
end
function isIncreasing(T)
local lengthT = tableLength(T)
for i = 1, lengthT-1 do
if T[i] >= T[i+1] then
return false
end
end
return true
end
---------------------------------
-- AUXILLIARY GLOBAL VARIABLES --
---------------------------------
-- No need to modify these.
seqLength = tableLength(seqFrames)
currentSeqFrames = {}
currentSeqDelays = {}
nextSeqComputed = true
isFirstFrame = true
firstFrame = 0
lastSeqReached = false
-- Note that action positions (and table indices in general) start from 1.
currentActionPos = 1
-------------------
-- ACTION MACROS --
-------------------
function Shake()
SetAccelZ(1023)
end
function PointAt(X,Y)
SetIRX(X)
SetIRY(Y)
end
function IsValidAction(action)
return inTable({"A", "+", "shake", "pNoLow",
"pMiddleCandy", "pRightCandy", "pYesCandy", "pOKHotel",
"pDice", "pYesDKStar", "pSBA", "pWario", "pStartGame"}, action)
end
function ActionToInput(action)
if not IsValidAction(action) then
MsgBox("WARNING: unknown action string.")
-- Frequent actions
elseif inTable({"A", "+"}, action) then
PressButton(action)
elseif action == "shake" then
Shake()
-- Situational pointing actions
elseif action == "pNoLow" then
-- Points at "No" on low textbox choices.
-- Examples: shops, or when asked about the secrets of a board.
PointAt(445, 665)
elseif action == "pMiddleCandy" then
-- When carrying one or three candies,
-- points at the one in the middle.
PointAt(480, 450)
elseif action == "pRightCandy" then
-- When carrying two candies,
-- points at the one on the right.
PointAt(400, 450)
elseif action == "pYesCandy" then
-- Points at the "Yes" option when confirming to use a candy.
PointAt(540, 665)
elseif action == "pDice" then
-- When carrying at least one candy,
-- points at the dice block option.
PointAt(475, 600)
elseif action == "pYesDKStar" then
-- Points at the "Yes" option when getting a star on DK's board.
PointAt(562, 475)
elseif action == "pOKHotel" then
-- Points at the "OK" option when investing in a hotel.
PointAt(280, 650)
-- Starting menus
elseif action == "pSBA" then
-- Points at "Star Battle Arena" in the main menu.
PointAt(470, 409)
elseif action == "pWario" then
-- Points at Wario in the character selection menu.
PointAt(440, 358)
elseif action == "pStartGame" then
-- Points at "Start Game" in the character selection menu.
PointAt(320, 650)
end
end
---------------
-- FUNCTIONS --
---------------
-- PRINTING
function FramePrint()
-- Prints info about the current frame
local currentFrame = GetFrameCount()
local toPrint = currentActionPos
toPrint = toPrint .. " | " .. currentFrame
if currentActionPos <= seqLength then
toPrint = toPrint .. string.char(10) .. "Next: " .. currentSeqFrames[currentActionPos]
end
-- Table with the action positions at the left,
-- the current sequence frames in the middle and
-- the corresponding current delay amounts on the right.
toPrint = toPrint .. string.char(10) .. "-----------------------"
toPrint = toPrint .. string.char(10) .. "# | frame | delay |"
for pos = 1, seqLength do
toPrint = toPrint .. string.char(10) .. pos .. " | " .. currentSeqFrames[pos] .. " | " .. currentSeqDelays[pos] .. " | "
end
toPrint = toPrint .. string.char(10) .. "-----------------------"
toPrint = toPrint .. string.char(10) .. "(Total delay: " .. currentTotalDelay .. ")"
SetScreenText(toPrint)
end
function PrintCurrentSeqDelays()
-- Prints currentTotalDelay and table currentSeqDelays on a single line.
local toPrint = currentTotalDelay .. " ("
for pos, delay in ipairs(currentSeqDelays) do
toPrint = toPrint .. delay
if pos == seqLength then
toPrint = toPrint .. ")"
else
toPrint = toPrint .. ", "
end
end
SetScreenText(toPrint)
end
-- TESTING
function TestButtons()
-- Test function to check Wiimote inputs.
-- It should alternate between A and + presses.
local currentFrame = GetFrameCount()
if currentFrame % 2 == 0 then
PressButton("A")
else
PressButton("+")
end
end
-- COMPUTING
function InitializeCurrentSeq()
-- Initializes to the frame sequence with delays at their lower bounds everywhere.
firstFrame = GetFrameCount()
seqLength = tableLength(seqFrames)
currentTotalDelay = 0
currentSeqFrames = {}
currentSeqDelays = {}
for pos, _ in ipairs(seqFrames) do
currentSeqFrames[pos] = seqFrames[pos]
for i = 1, pos do
currentSeqFrames[pos] = currentSeqFrames[pos] + seqDelayLowerBounds[i]
end
currentSeqDelays[pos] = seqDelayLowerBounds[pos]
currentTotalDelay = currentTotalDelay + seqDelayLowerBounds[pos]
end
end
function ComputeCurrentSeq()
-- Computes table currentSeqFrames by starting from seqFrames
-- and adding the values from currentSeqDelays.
for pos, _ in ipairs(seqFrames) do
currentSeqFrames[pos] = seqFrames[pos]
for i = 1, pos do
currentSeqFrames[pos] = currentSeqFrames[pos] + currentSeqDelays[i]
end
end
end
function ComputeNextSeqDelays(pos)
-- Finds the next sequence of delays (in lexicographic order).
-- /!\ recursive
if currentSeqDelays[pos] < math.min(seqDelayUpperBounds[pos], maxTotalDelay) then
currentSeqDelays[pos] = currentSeqDelays[pos] + 1
currentTotalDelay = currentTotalDelay + 1
elseif pos == 1 then
-- Trying to increment the final sequence.
-- Then all the requested frame sequences have been considered.
lastSeqReached = true
else
currentTotalDelay = currentTotalDelay - currentSeqDelays[pos] + seqDelayLowerBounds[pos]
currentSeqDelays[pos] = seqDelayLowerBounds[pos]
ComputeNextSeqDelays(pos-1)
end
end
function PrepareForNextSeq()
-- Finds the next delay sequence (in lexicographic order)
-- with a total delay higher than or equal to minTotalDelay
-- and lower than or equal to maxTotalDelay.
-- /!\ can cancel the script
repeat
ComputeNextSeqDelays(seqLength)
until (currentTotalDelay >= minTotalDelay and currentTotalDelay <= maxTotalDelay) or lastSeqReached
if lastSeqReached then
CancelScript()
else
ComputeCurrentSeq()
currentActionPos = 1
end
end
function SanityChecks()
-- If you want these sanity checks to be displayed,
-- make sure to frame advance exactly once after starting the script.
-- If a save state is loaded between the script start and the frame advance
-- then the warning messages will still be displayed.
if seqLength ~= tableLength(seqActions) or
seqLength ~= tableLength(seqDelayUpperBounds) or
seqLength ~= tableLength(seqDelayLowerBounds) then
SetScreenText("WARNING: input table sizes do not match.")
elseif not isIncreasing(seqFrames) then
SetScreenText("WARNING: the given frame sequence is not increasing.")
else
for pos = 1, seqLength do
if not IsValidAction(seqActions[pos]) then
SetScreenText("WARNING: action number " .. pos .. " is not recognized.")
return
end
if seqDelayLowerBounds[pos] > seqDelayUpperBounds[pos] then
SetScreenText("WARNING: empty delay range for action number " .. pos .. ".")
return
end
end
end
end
---------------
-- CALLBACKS --
---------------
function onScriptStart()
MsgBox("Script started: Mario Party 8 RNG manipulation helper")
SetScreenText("Script started.")
end
function onScriptCancel()
MsgBox("Script ended: Mario Party 8 RNG manipulation helper")
SetScreenText("Script ended.")
end
-- MAIN ROUTINE
-- Overall job: performs the currently tested sequence of delays.
function onScriptUpdate()
local currentFrame = GetFrameCount()
if firstFrame == currentFrame then
SanityChecks()
else
isFirstFrame = false
end
-- If loading back to the beginning:
-- test the next delay sequence (in lexicographic order).
if currentFrame < seqFrames[1] and not nextSeqComputed then
PrepareForNextSeq()
nextSeqComputed = true
elseif currentFrame >= seqFrames[1] and nextSeqComputed then
nextSeqComputed = false
end
-- When going past the final sequence frame, ignore until reloading.
if currentActionPos > seqLength then
FramePrint()
return
end
-- When going past a nonfinal sequence frame, update to the next one.
if currentFrame > currentSeqFrames[currentActionPos] then
currentActionPos = currentActionPos + 1
-- When reaching a sequence frame, perform the corresponding action.
elseif currentFrame == currentSeqFrames[currentActionPos] then
ActionToInput(seqActions[currentActionPos])
end
if not isFirstFrame then
FramePrint()
end
end
function onStateLoaded()
end
function onStateSaved()
end
----------
-- MAIN --
----------
InitializeCurrentSeq()