Custom Lua Moveset - Where To Start?

Status
Not open for further replies.

Spring E. Thing

Member
Modder
Apr 8, 2024
41
5
230
Hey guys, I've been interested in doing some Lua for a custom moveset + character for months now. I've tried a few times, but every time I try and get a start I really stick with it for like a day or two but then just get so overwhelmed and fatigued. Normally in this situation I'd cling to documentation like a life preserver, but I've found that the Lua documentation in the various git repos related to DX Co-Op are sparse on details for anything but the broad concepts. My second port of call was to download existing Lua custom moveset + character mods and examine those, but the two that I used (Pac-Man and Bowser) were a slightly messy rework of an existing character and far advanced beyond my expectations without feeling that sense of overwhelm respectively. My experience with Lua is minimal, but my experience in other programming languages such as C# especially is tenured with a year or two on-and-off in C++ and God knows how long noodling about in Python back in high school, so what I'm trying to say is that I feel like adapting to Lua for such a big project isn't the bulk of my issue.

Are there any exercises I can do to "git gud"? Can some helpful soul here on the forums set an activity for me to do and maybe guide me along? Ideally, I'd like enough help such that I can implement an entirely custom action that doesn't replace any existing actions and will be perfectly synced over the network, even if that action is ultra-simple. I'm not even worrying about the custom models or sounds required for this project yet because baby steps yknow?

Thanks for reading!
 
  • Like
Reactions: JaxInvasion
Solution
Lua is very easy to understand, so I assure you that won't be the problem! You're 100% right about the documentation, it doesn't go that much into depth. Firstly, if you haven't, setup vscode. Second, head here, and read up on section 1 and 3. Section 2 contains each and every entry in the MarioState class, this'll be handy later. For an optional step, read the guide on hooks aswell, as it contains many hooks that may be relevant to making a moveset (depending on what you're making).

Here are some basic stuff to know when creating/understanding a moveset, also, m refers to the MarioState:

m.action is the current action mario is in, this will come handy if checking if mario is diving, etc.
set_mario_action is a...
Lua is very easy to understand, so I assure you that won't be the problem! You're 100% right about the documentation, it doesn't go that much into depth. Firstly, if you haven't, setup vscode. Second, head here, and read up on section 1 and 3. Section 2 contains each and every entry in the MarioState class, this'll be handy later. For an optional step, read the guide on hooks aswell, as it contains many hooks that may be relevant to making a moveset (depending on what you're making).

Here are some basic stuff to know when creating/understanding a moveset, also, m refers to the MarioState:

m.action is the current action mario is in, this will come handy if checking if mario is diving, etc.
set_mario_action is a function that allows you to set mario's action.
m.controller.buttonDown/m.controller.buttonPressed is pretty self explanatory. You need to use a bitwise operator, so you'll have to do this to see if a input is being held:

Code:
if m.controller.buttonPressed & A_BUTTON ~= 0 then
    -- code here
end


Those are pretty important fundamentals for making/understanding movesets. I'd recommend starting out with making modifications to already existing movesets. Here's an example moveset with comments explaining everything:

Code:
-- name: Simple Moveset
-- incompatible: moveset
-- description: Basic moveset for beginners to learn how to make their own movesets!

local function mario_update(m)

    -- if we landed from a ground pound, and the button pressed is the a button, set action to triple jump
    if m.action == ACT_GROUND_POUND_LAND and m.controller.buttonPressed & A_BUTTON ~= 0 then
        -- set mario's upward velocity to 30
        m.vel.y = 30
        set_mario_action(m, ACT_TRIPLE_JUMP, 0)
    end

    -- if we are twirling and hit the z trigger, make mario go down faster
    if m.action == ACT_TWIRLING and m.controller.buttonDown & Z_TRIG ~= 0 then
        -- set mario's vertical velocity to go down faster
        m.vel.y = -50
        -- spawn mist particles
        spawn_mist_particles_variable(0.1, 0, 11)
    end

    -- if we land from a twirl, and the botton down is the z trig, set action to ground pound

    -- we check the action timer here so it will only enter ground pound if you were hitting z
    -- in the twirling action or you hit it frame 1
    if m.action == ACT_TWIRL_LAND and m.controller.buttonDown & Z_TRIG ~= 0 and m.actionTimer == 0 then
        set_mario_action(m, ACT_GROUND_POUND, 0)
        -- I think this is so the ground pound is instant, although it's pretty bad code
        -- so don't take example from this :/
        m.actionTimer = 5000
    end

    -- the action timer doesn't go up in this action, so do that here
    if m.action == ACT_TWIRL_LAND then
        m.actionTimer = m.actionTimer + 1
    end

    -- if we ground pounded, and hit B, then dive
    if m.action == ACT_GROUND_POUND and m.controller.buttonPressed & B_BUTTON ~= 0 then
        -- set upward velocity
        m.vel.y = 36
        -- set forward velocity
        m.forwardVel = 25
        -- set face angle to the angle the joystick is at
        m.faceAngle.y = m.intendedYaw
        -- add a mist to mario's particle flags
        m.particleFlags = m.particleFlags | PARTICLE_MIST_CIRCLE
        -- set mario's action
        set_mario_action(m, ACT_DIVE, 0)
    end
end

-- hooks
hook_event(HOOK_MARIO_UPDATE, mario_update)

For any item in the mario state you don't understand, read section 2 of the gMarioStates guide above, as that should give notes on what each item does.

Hope this helps! If you have any questions tell me :]
 
Solution
Lua is very easy to understand, so I assure you that won't be the problem! You're 100% right about the documentation, it doesn't go that much into depth. Firstly, if you haven't, setup vscode. Second, head here, and read up on section 1 and 3. Section 2 contains each and every entry in the MarioState class, this'll be handy later. For an optional step, read the guide on hooks aswell, as it contains many hooks that may be relevant to making a moveset (depending on what you're making).

Here are some basic stuff to know when creating/understanding a moveset, also, m refers to the MarioState:

m.action is the current action mario is in, this will come handy if checking if mario is diving, etc.
set_mario_action is a function that allows you to set mario's action.
m.controller.buttonDown/m.controller.buttonPressed is pretty self explanatory. You need to use a bitwise operator, so you'll have to do this to see if a input is being held:

Code:
if m.controller.buttonPressed & A_BUTTON ~= 0 then
    -- code here
end


Those are pretty important fundamentals for making/understanding movesets. I'd recommend starting out with making modifications to already existing movesets. Here's an example moveset with comments explaining everything:

Code:
-- name: Simple Moveset
-- incompatible: moveset
-- description: Basic moveset for beginners to learn how to make their own movesets!

local function mario_update(m)

    -- if we landed from a ground pound, and the button pressed is the a button, set action to triple jump
    if m.action == ACT_GROUND_POUND_LAND and m.controller.buttonPressed & A_BUTTON ~= 0 then
        -- set mario's upward velocity to 30
        m.vel.y = 30
        set_mario_action(m, ACT_TRIPLE_JUMP, 0)
    end

    -- if we are twirling and hit the z trigger, make mario go down faster
    if m.action == ACT_TWIRLING and m.controller.buttonDown & Z_TRIG ~= 0 then
        -- set mario's vertical velocity to go down faster
        m.vel.y = -50
        -- spawn mist particles
        spawn_mist_particles_variable(0.1, 0, 11)
    end

    -- if we land from a twirl, and the botton down is the z trig, set action to ground pound

    -- we check the action timer here so it will only enter ground pound if you were hitting z
    -- in the twirling action or you hit it frame 1
    if m.action == ACT_TWIRL_LAND and m.controller.buttonDown & Z_TRIG ~= 0 and m.actionTimer == 0 then
        set_mario_action(m, ACT_GROUND_POUND, 0)
        -- I think this is so the ground pound is instant, although it's pretty bad code
        -- so don't take example from this :/
        m.actionTimer = 5000
    end

    -- the action timer doesn't go up in this action, so do that here
    if m.action == ACT_TWIRL_LAND then
        m.actionTimer = m.actionTimer + 1
    end

    -- if we ground pounded, and hit B, then dive
    if m.action == ACT_GROUND_POUND and m.controller.buttonPressed & B_BUTTON ~= 0 then
        -- set upward velocity
        m.vel.y = 36
        -- set forward velocity
        m.forwardVel = 25
        -- set face angle to the angle the joystick is at
        m.faceAngle.y = m.intendedYaw
        -- add a mist to mario's particle flags
        m.particleFlags = m.particleFlags | PARTICLE_MIST_CIRCLE
        -- set mario's action
        set_mario_action(m, ACT_DIVE, 0)
    end
end

-- hooks
hook_event(HOOK_MARIO_UPDATE, mario_update)

For any item in the mario state you don't understand, read section 2 of the gMarioStates guide above, as that should give notes on what each item does.

Hope this helps! If you have any questions tell me :]

Alrighty, thanks a ton for this, it's really helped clear my head on the matter and provided a very clear explanation and link as to what does what in-game.
Anyway, I used this code block as well as some looking-over the Pac-Man moveset mod to develop a custom action. The intent of the action is that, when A is pressed during the start of a ground pound, Mario will move in a spiral pattern for about a second and then go into a jump. This action isn't meant to be good or even make sense, but it's of course just as a test of making something that works.

Here's the code block I came up with for this concept:

Code:
-- name: Simple Moveset
-- description: Basic moveset for beginners to learn how to make their own movesets!

ACT_SIMPLEMOVESET_SPIRAL = allocate_mario_action(ACT_FLAG_AIR)
gStateExtras = {}

local function lerp(a,b,i)
    return a+((b-a)*i)
end

local function mario_update(m)
    if m.action == ACT_GROUND_POUND and m.controller.buttonPressed & A_BUTTON ~=0 then
        local e = gStateExtras[m.playerIndex]
        e.spiralTime_cur = 0
        e.startX = m.pos.x
        e.startY = m.pos.y
        set_mario_animation(m, MARIO_ANIM_FORWARD_SPINNING)
        set_mario_action(m,ACT_SIMPLEMOVESET_SPIRAL,0)
    end

end

local function act_simplemoveset_spiral(m)
    local e = gStateExtras[m.playerIndex]

    local spiralExag_cur = lerp(0,e.spiralExag_max,(1/e.spiralTime_max)*e.spiralTime_cur)

    m.pos.x = e.startX + (math.sin(e.spiralTime_cur) * spiralExag_cur)
    m.pos.y = e.startY + (math.cos(e.spiralTime_cur) * spiralExag_cur)

    e.spiralTime_cur = e.spiralTime_cur + 1

    if e.spiralTime_cur>=e.spiralTime_max then
        set_mario_action(m,ACT_JUMP,0)
    end
end

-- hooks
hook_event(HOOK_MARIO_UPDATE, mario_update)

-- actions
hook_mario_action(ACT_SIMPLEMOVESET_SPIRAL, act_simplemoveset_spiral)

for i=0,(MAX_PLAYERS-1) do
    gStateExtras[i] = {}
    local e = gStateExtras[i]
    e.startX = 0
    e.startY = 0
    e.spiralTime_cur = 0
    e.spiralExag_max = 100
    e.spiralTime_max = 30
end

This code block as-is ALMOST works at least in a 1-person server, but it would seem as though there is a major flaw in that setting the channels of m.pos directly does not affect Mario's visual representation along with his data representation in-engine (see below):

Be all this as it may, I have a few questions I'd love to get some insight on:
1. What am I missing with the code block to get bare minimum functionality towards my testing goal as-is? Am I supposed to set Mario's position through a method instead?

2. The initializing I'm doing of gStateExtras[] is something I lifted from the Pac-Man source code as is the concept of gStateExtras[] in general. Surely, if any player were to leave or join AFTER this Lua code is loaded in, the initialized table would be of the incorrect length and indexing anyway right? Does this initializing really need to happen or am I good to just start setting and getting variables from the table whenever during execution?

3. Adding on to my 2nd point, surely accessing gStateExtras[] by an index of m.playerIndex simply won't do due to the join/leave scenario and would desync the Lua from the rest of the game state right?

4. What do action flags actually do? The one I picked for the custom action I set up in my code block I just picked on a guess as to what would 'fit' my action, but I'm not sure how the game is affected if, say, no action flags were selected in the bitwise operation.

5. Anything else SUPER concerning you want me to nip in the bud before I go on?
 
Last edited:
Sorry for the late response.

The initializing I'm doing of gStateExtras[] is something I lifted from the Pac-Man source code as is the concept of gStateExtras[] in general. Surely, if any player were to leave or join AFTER this Lua code is loaded in, the initialized table would be of the incorrect length and indexing anyway right? Does this initializing really need to happen or am I good to just start setting and getting variables from the table whenever during execution?
It's a good idea to initialize the table. You can skip initializing the table, but only do so if you are 100% confident the variables in there are never read beforehand. It's just safer to initialize the table. Hopefully that answers that question.
Adding on to my 2nd point, surely accessing gStateExtras[] by an index of m.playerIndex simply won't do due to the join/leave scenario and would desync the Lua from the rest of the game state right?
Just so you know gStateExtras isn't synced. The only time gStateExtras is read is in the spiral action, which can only be gotten to by setting the gStateExtras variable, so in this case, it's fine. Normally, you'd check if that player is connected, if they aren't, reset that index of the table. Imo though the table's purpose here feels weird, which you'll see when I give a optimized/fixed example of what you want.

I'll answer question 1 and 4 with this

The way you are creating an action and setting the action is great, and that's pretty much how good it gets. ACT_FLAG_AIR is the perfect action flag as well, however you may want to add ACT_FLAG_ATTACKING if you want that.

Setting mario's position generally doesn't need to be hardcoded, you should instead set mario's velocity (m.vel.x/y/z and m.forwardVel) and use perform_air_step(MarioState, arg) (I'm unsure what the second arg is, I typically set it to 0 though). I'll show you that in the updated code. If you ever need to set mario's position, you can set all velocities to 0 and run `perform_air_step`, or update the gfx pos manually by editing m.marioObj.header.gfx.pos

gStateExtras is overcomplicating this mod a lot, it really isn't needed and the same goal can be accomplished without it. Something like gStateExtras imo should only be used when you have a group of variables used in multiple places related to the same thing.

Here's some new code I wrote up, with comments explaining why I changed what I did:

Code:
-- name: Simple Moveset
-- description: Basic moveset for beginners to learn how to make their own movesets!

ACT_SPIRAL = allocate_mario_action(ACT_FLAG_AIR)
-- removed gStateExtras as it was not needed for
-- using the velocity method.

-- also removed the lerp function, however that doesn't mean it was bad
-- I just didn't know how it worked. Making functions like that is a great
-- practice, and will help with keeping your projects nice and tidy
-- just make sure to comments what each function does and how to use it :)

local function mario_update(m)
    -- I simplified this, gStateExtras wasn't needed so removed that,
    -- and I moved the animation to the action, as that's where it should be
    if m.action == ACT_GROUND_POUND and m.controller.buttonPressed & A_BUTTON ~=0 then
        set_mario_action(m, ACT_SPIRAL, 0)
    end
end

---@param m MarioState
local function act_spiral(m)

    -- set mario's animation here instead of above
    set_mario_animation(m, MARIO_ANIM_FORWARD_SPINNING)

    -- now this is pretty unrecognizable, so lemme explain
    -- ensure mario doesn't move in the y axis
    m.vel.y = 0
    -- set speedx/z to a random value between -50 and 50
    local speedX = math.random(-50, 50)
    local speedZ = math.random(-50, 50)
    -- set mario's horizontal velocity the the random speed vars
    m.vel.x = speedX
    m.vel.z = speedZ
    -- perform air step
    perform_air_step(m, 0)

    -- increase action timer
    m.actionTimer = m.actionTimer + 1

    -- if action timer is greater than a second, set action to jump
    -- I know 1 * 30 = 30, however I perfer having the leading number
    -- always existing, as that shows the amount of seconds at a glance
    -- this is personal preference though.
    if m.actionTimer >= 1 * 30 then
        set_mario_action(m, ACT_JUMP, 0)
    end
end

-- hooks
hook_event(HOOK_MARIO_UPDATE, mario_update)

-- actions
hook_mario_action(ACT_SPIRAL, act_spiral)

Don't know how to send a video here, so sorry bout that :/.

Another thing I noticed in your code is math.sin/math.cos. These should be sins/coss, as those are the sm64 functions that take in a angle value between -65536 and 65536, and not radians.

I hope I answered all your questions here, and hopefully I didn't make this longer than it needs to be lol. There wasn't anything super concerning, your code was great, just needed some other knowledge that i didn't give you in my first message :].
 
  • Like
Reactions: Spring E. Thing
Sorry for the late response.


It's a good idea to initialize the table. You can skip initializing the table, but only do so if you are 100% confident the variables in there are never read beforehand. It's just safer to initialize the table. Hopefully that answers that question.

Just so you know gStateExtras isn't synced. The only time gStateExtras is read is in the spiral action, which can only be gotten to by setting the gStateExtras variable, so in this case, it's fine. Normally, you'd check if that player is connected, if they aren't, reset that index of the table. Imo though the table's purpose here feels weird, which you'll see when I give a optimized/fixed example of what you want.

I'll answer question 1 and 4 with this

The way you are creating an action and setting the action is great, and that's pretty much how good it gets. ACT_FLAG_AIR is the perfect action flag as well, however you may want to add ACT_FLAG_ATTACKING if you want that.

Setting mario's position generally doesn't need to be hardcoded, you should instead set mario's velocity (m.vel.x/y/z and m.forwardVel) and use perform_air_step(MarioState, arg) (I'm unsure what the second arg is, I typically set it to 0 though). I'll show you that in the updated code. If you ever need to set mario's position, you can set all velocities to 0 and run `perform_air_step`, or update the gfx pos manually by editing m.marioObj.header.gfx.pos

gStateExtras is overcomplicating this mod a lot, it really isn't needed and the same goal can be accomplished without it. Something like gStateExtras imo should only be used when you have a group of variables used in multiple places related to the same thing.

Here's some new code I wrote up, with comments explaining why I changed what I did:

Code:
-- name: Simple Moveset
-- description: Basic moveset for beginners to learn how to make their own movesets!

ACT_SPIRAL = allocate_mario_action(ACT_FLAG_AIR)
-- removed gStateExtras as it was not needed for
-- using the velocity method.

-- also removed the lerp function, however that doesn't mean it was bad
-- I just didn't know how it worked. Making functions like that is a great
-- practice, and will help with keeping your projects nice and tidy
-- just make sure to comments what each function does and how to use it :)

local function mario_update(m)
    -- I simplified this, gStateExtras wasn't needed so removed that,
    -- and I moved the animation to the action, as that's where it should be
    if m.action == ACT_GROUND_POUND and m.controller.buttonPressed & A_BUTTON ~=0 then
        set_mario_action(m, ACT_SPIRAL, 0)
    end
end

---@param m MarioState
local function act_spiral(m)

    -- set mario's animation here instead of above
    set_mario_animation(m, MARIO_ANIM_FORWARD_SPINNING)

    -- now this is pretty unrecognizable, so lemme explain
    -- ensure mario doesn't move in the y axis
    m.vel.y = 0
    -- set speedx/z to a random value between -50 and 50
    local speedX = math.random(-50, 50)
    local speedZ = math.random(-50, 50)
    -- set mario's horizontal velocity the the random speed vars
    m.vel.x = speedX
    m.vel.z = speedZ
    -- perform air step
    perform_air_step(m, 0)

    -- increase action timer
    m.actionTimer = m.actionTimer + 1

    -- if action timer is greater than a second, set action to jump
    -- I know 1 * 30 = 30, however I perfer having the leading number
    -- always existing, as that shows the amount of seconds at a glance
    -- this is personal preference though.
    if m.actionTimer >= 1 * 30 then
        set_mario_action(m, ACT_JUMP, 0)
    end
end

-- hooks
hook_event(HOOK_MARIO_UPDATE, mario_update)

-- actions
hook_mario_action(ACT_SPIRAL, act_spiral)

Don't know how to send a video here, so sorry bout that :/.

Another thing I noticed in your code is math.sin/math.cos. These should be sins/coss, as those are the sm64 functions that take in a angle value between -65536 and 65536, and not radians.

I hope I answered all your questions here, and hopefully I didn't make this longer than it needs to be lol. There wasn't anything super concerning, your code was great, just needed some other knowledge that i didn't give you in my first message :].
Thanks again for your response! It's pretty late here now so I won't be doing anything else on any of this until tomorrow (mostly just making this post to let you know I appreciate the continued effort demonstrated), however I would like to point out that I think my omission of explaining what the lerp() method does has lead you to further misunderstand the SPIRAL action and why I can't just set the X and Z velocities to random values for every tick execution of the action.
The lerp() method (as is found in many maths libraries in and out of Lua) takes in a start value, an end value, and a normalized 0 to 1 value to find in the range of start to end. For example, inputting a start value of 5, an end value of 7, and a normalized value of 0.5 would return 6 since 6 = 5 + ((7-5)*0.5) with 7-5 being the signed span of the start and end values (the nifty thing about the lerp() method implementation I used being that it works even if the end value is less than the start value or the 0 to 1 value is actually outside the range of 0 to 1).
Consequently, the point of using lerp() in my original code block was so that as the timer for the action progressed (pointless as it was given that action_timer exists, ty for that tip), the X and Y values chosen to set Mario's position to would only get more extreme and act as though an expanding circle, in-practice creating a spiral motion effect when put into the context of sin() and cos(), which is furthermore why I had to cache values before the start of the SPIRAL action in the extras table to give the action some sort of memory of what state it was in when it began to then transform to a lerp() start value.
Anyway, setting the animation during the action itself makes a lot more sense in terms of designs and patterns for code at the least, I just figured doing it every tick of the action would be inefficient, but maybe the source is smart enough to not change animations if the new action being set is the same as the current animation?
Regardless of interpretation, though, I will try out your code block tomorrow since at the least it should solve the problem of Mario not moving where I'm telling him to go even if the place he's being told to go in your interpretation isn't what I had in mind. Your advice for directly setting his position SHOULD sort me out regardless of your own interpretations anyway, so thanks a ton once more!
 
  • Like
Reactions: EmeraldLockdown
Alrighty, so since I already had VSCode configured for this work before this thread even began, I took the advice of EmeraldLockdown in addition to just scooping through the documented parts of the various .lua definition files and have produced the following:

Code:
-- name: Spiral Mario
-- description: Spiral Mario Can Spin!
ACT_SIMPLEMOVESET_SPIRAL = allocate_mario_action(ACT_FLAG_AIR)

-- gStateExtras shall contain player-specific variables that will change as the script is executed
-- i don't need to know network sync logic since each index of the array respresents a player in
-- the game and will only change when something about that player changes that is already network-synced
gStateExtras = {}

-- these local variables are expected to be constant
-- they have no association to any particular player and so if i wanted them to change during script
-- execution and have those changes sync over the server then i'd need to come to understand the game's
-- network sync methods (todo on the condition that i ever want shared variables that change at runtime)
local spiralTime_max = 60
local spiralExag_max = 200
local spiralTimeExag = 0.3

-- standard implementation of a lerp calculation
-- IDK if the Lua math library has this already but as far as I could tell it does not
local function lerp(a,b,i)
    return a+((b-a)*i)
end

local function mario_update(m)
    if m.action == ACT_GROUND_POUND and m.controller.buttonPressed & A_BUTTON ~=0 then
        local e = gStateExtras[m.playerIndex]
        e.spiralTime_cur = 0
        e.startX = m.pos.x
        e.startY = m.pos.y
        set_mario_animation(m, MARIO_ANIM_FORWARD_SPINNING)
        set_mario_action(m,ACT_SIMPLEMOVESET_SPIRAL,0)
    end
end

local function act_simplemoveset_spiral(m)
    -- get the extra variables for the player doing the action
    local e = gStateExtras[m.playerIndex]

    -- set the current spiral radius to be representative of how far along the spiral timer is
    local spiralExag_cur = lerp(0,spiralExag_max,(1/spiralTime_max)*e.spiralTime_cur)

    --zero the velocity values because perform_air_step will use them for gravity simulation when i only want it to simulate collision
    m.vel.x=0
    m.vel.y=0
    m.vel.z=0

    -- set mario's X and Y directly with no consideration for direction being faced (todo)
    m.pos.x = e.startX + (math.sin(e.spiralTime_cur*spiralTimeExag) * spiralExag_cur)
    m.pos.y = e.startY + (math.cos(e.spiralTime_cur*spiralTimeExag) * spiralExag_cur)

    -- update mario's graphical self to match his data self
    m.marioObj.header.gfx.pos.x = m.pos.x
    m.marioObj.header.gfx.pos.y = m.pos.y

    -- do world collision and gravity for air movement
    perform_air_step(m,0)

    e.spiralTime_cur = e.spiralTime_cur + 1

    if e.spiralTime_cur>=spiralTime_max then
        set_mario_action(m,ACT_JUMP,0)
    end
end

local function initExtrasAtIndex(i)
    gStateExtras[i] = {}
    local e = gStateExtras[i]
    e.startX = 0
    e.startY = 0
    e.spiralTime_cur = 0
end

-- every time a player connects or disconnects, the index of gStateExtras that player occupies or occupied respectively will
-- be reinitialized. technically this only needs to happen on connection and not disconnection, but i like the data purity
-- of doing it for both and i doubt it's any kind of performance hit.
local function player_changeConnection(m)
    initExtrasAtIndex(m.playerIndex)
end

-- hooks
hook_event(HOOK_MARIO_UPDATE, mario_update)
hook_event(HOOK_ON_PLAYER_CONNECTED, player_changeConnection)
hook_event(HOOK_ON_PLAYER_DISCONNECTED, player_changeConnection)

-- actions
hook_mario_action(ACT_SIMPLEMOVESET_SPIRAL, act_simplemoveset_spiral)

-- technically unneccessary with c/dc hooks but this establishes the length of the gStateExtras table to be MAX_PLAYERS
for i=0,(MAX_PLAYERS-1) do
    initExtrasAtIndex(i)
end

The block above works exactly as I first expected of it to in-engine which is wonderful to see. For the reference of others, I came to realize after posting one of my older posts to this thread that the gStateExtras[] table would NOT desync upon players leaving/joining since it is initialized at the bottom of the code block to be as long as the maximum number of players the current server hosting config allows, a value which does not change during an active gameplay (right?). The table itself is not synced as EmeraldLockdown correctly acknowledged, I think I was using the wrong term when I wrote that, what I meant to describe was the fact that gStateExtras[] will have each index represent a player in the server and that each player will ALWAYS read from and write to the "correct" index such that the player doesn't experience corruption when other players leave and/or join the server. Additionally, I discovered the hooks for when players leave and join the server and chose to re-initialize the index within gStateExtras[] that was related to a particular player upon either of these things occurring. I trimmed down gStateExtras[] to only what was neccessary to be changed during active gameplay leaving the constants as plain-old local variables. I still don't fully understand the point of the action flags even if I did set the right one when defining my custom action (is it a cue to the rest of the game as to how to interact with Mario during the action?). I didn't use the action timer in the MarioState struct as advised by EmeraldLockdown but that's probably the choice I made that I am most flexible on since clearly .actionTimer is the INTENDED way to progress time during an action however it would seem as though the only actual reason to do it the "intended" way is for interop with other mods that read the .actionTimer and that the value is nothing more than an int tracking consumed ticks.
Lastly, I'd like to mention that I didn't swap the Lua math library implementations of sin() and cos() to the SM64DX implementations as clarified by EmeraldLockdown but I have no objection to doing so, it was simply as matter of 'this works, so why change it?' considering that the input for the SM64DX implementations expects an int angle DOOM-engine style rather than a radian float.

I don't know how moderators will feel about this idea, but I would like to continue using this thread to post about as I develop this into a greater mod of SOME kind and simply pipe up ONLY when I encounter an issue that leaves me stumped.
 
TL;DR Why are my custom Lua Mario actions causing Mario to immediately die when those actions are performed over a floor of quicksand?

It's been a day or two, I've been hard at work amongst other tasks in my life improving what I have here just for the fun of it and to improve my skills even if the custom move itself isn't very useful.

Here's how the move acts as of writing this...:

...and here's how the source code for this whole mod looks (need to look into splitting the .lua files and how that affects the global namespace sometime soon):
Code:
-- name: Spiral Mario
-- description: Spiral Mario Can Spin!

ACT_SPIRALMARIO_SPIRAL = allocate_mario_action(ACT_FLAG_AIR)
ACT_SPIRALMARIO_FLING = allocate_mario_action(ACT_FLAG_AIR)

gStateExtras = {}

local function lerp(a,b,i)
    return a+((b-a)*i)
end

local function invlerp(a,b,x)
    return (x-a)/(b-a)
end

local function eulerToForDir(euler)
    local x =0
    local z  =0

    if(euler<180) then
        z = invlerp(90,0,euler)

        if(euler<90) then
            x = invlerp(0,90,euler)
        else
            x = invlerp(180,90,euler)
        end
    else
        z = invlerp(270,360,euler)

        if(euler>=270) then
            x = invlerp(360,270,euler)
        else
            x = invlerp(180,270,euler)
        end

        x=x*-1
    end
    
    local ret = {}
    vec3f_set(ret,x,0,z)
    vec3f_normalize(ret)
    return ret
end

local function intAngleToEuler(intAng)
    local angI = invlerp(-32768,32767,intAng)
    return lerp(0,360,angI)
end

local function initExtrasByIndex(i)
    gStateExtras[i] = {}
    local e = gStateExtras[i]
    e.startPos = {}
    vec3f_set(e.startPos,0,0,0)
    e.spiralTime_cur = 0
    e.facingEulerY = 0
    e.flingVec = {}
    e.spiralOvalProgressTicks = 0
    vec3f_set(e.flingVec,0,0,0)
    return gStateExtras[i]
end

local function initExtrasByPlayer(m)
    local player = initExtrasByIndex(m.playerIndex)
    player.facingEulerY = intAngleToEuler(m.faceAngle.y)
    return player
end

local function mario_update(m)
    if m.action == ACT_GROUND_POUND and m.controller.buttonPressed & A_BUTTON ~=0 then
        local e = initExtrasByPlayer(m)
        vec3f_copy(e.startPos,m.pos)
        set_mario_action(m,ACT_SPIRALMARIO_SPIRAL,0)
    end

end

local function act_spiralmario_common_airstep(m,prevToCurVec)
    vec3f_copy(m.vel,prevToCurVec)
    local stepResult = perform_air_step(m,0)

    -- vec3f are a class and therefore passed by reference, so changing these values DOES
    -- make a difference in the method that called this one
    vec3f_copy(prevToCurVec,m.vel)

    -- recreation of only what i need from common_air_action_step from https://github.com/n64decomp/sm64/blob/master/src/game/mario_actions_airborne.c
    -- without having to rely on forwardVel since IDK what value it should be at this time
    if(stepResult == AIR_STEP_HIT_WALL) then
        set_mario_action(m,ACT_GROUND_BONK,0)
    elseif (stepResult== AIR_STEP_HIT_LAVA_WALL) then
        lava_boost_on_wall(m)
    elseif (stepResult == AIR_STEP_LANDED)  then
        set_mario_action(m,ACT_JUMP_LAND_STOP,0)
    end
end

local function act_spiralmario_common_setup(m)
    set_mario_animation(m,MARIO_ANIM_FORWARD_SPINNING)
    local e = gStateExtras[m.playerIndex]
    return e
end

local function act_spiralmario_fling(m)
    local e = act_spiralmario_common_setup(m)
    act_spiralmario_common_airstep(m,e.flingVec)
end

local function act_spiralmario_spiral(m)
    local e = act_spiralmario_common_setup(m)

    local spiralTime_cur_int = math.floor(e.spiralTime_cur)
    local spiralTime_max = 17
    local spiralExag_max = 275
    local timeStep = 0.45
    local spiralSndInverseRate = 3
    local spiralOvalMultMin = 1
    local spiralOvalMultMax = 1.3

    if(spiralTime_cur_int % math.max(math.floor(spiralSndInverseRate),1) == 0) then
        play_sound(SOUND_ACTION_THROW,m.marioObj.header.gfx.cameraToObject)
    end

    local spiralExag_cur = lerp(0,spiralExag_max,(1/spiralTime_max)*e.spiralTime_cur)

    local dir_for = {}
    vec3f_copy(dir_for,eulerToForDir(e.facingEulerY))
    local dir_up = {}
    vec3f_set(dir_up,0,1,0)
    vec3f_normalize(dir_up)
    local finalPos = {}
    vec3f_copy(finalPos,e.startPos)
    vec3f_mul(dir_for,(math.sin(e.spiralTime_cur) * spiralExag_cur))
    vec3f_mul(dir_up,(math.cos(e.spiralTime_cur) * spiralExag_cur * lerp(spiralOvalMultMin,spiralOvalMultMax,invlerp(0,spiralTime_max,e.spiralOvalProgressTicks))))
    vec3f_add(finalPos,dir_for)
    vec3f_add(finalPos,dir_up)

    local prevPos = {}
    vec3f_copy(prevPos,m.pos)
    local moveVec = {}
    vec3f_copy(moveVec,finalPos)
    vec3f_sub(moveVec,prevPos)

    act_spiralmario_common_airstep(m,moveVec)

    e.spiralTime_cur = e.spiralTime_cur + timeStep

    if(m.controller.buttonDown & A_BUTTON ~=0) then
        e.spiralOvalProgressTicks = e.spiralOvalProgressTicks+timeStep
    end

    if e.spiralTime_cur>=spiralTime_max or m.controller.buttonPressed & B_BUTTON ~=0 then
        local flingSpeed = vec3f_dist(prevPos,m.pos) --NOTE: don't need to divide by time since it's 1 tick AKA divided by 1
        vec3f_copy(e.flingVec,m.pos)
        vec3f_sub(e.flingVec,prevPos)
        vec3f_normalize(e.flingVec)
        vec3f_mul(e.flingVec,flingSpeed)
        play_sound_if_no_flag(m,SOUND_MARIO_YAHOO,MARIO_MARIO_SOUND_PLAYED)
        play_sound(SOUND_GENERAL_BOING1,m.marioObj.header.gfx.cameraToObject)
        set_mario_action(m,ACT_SPIRALMARIO_FLING,0)
    end
end

hook_event(HOOK_MARIO_UPDATE, mario_update)
hook_event(HOOK_ON_PLAYER_CONNECTED, initExtrasByPlayer)
hook_event(HOOK_ON_PLAYER_DISCONNECTED, initExtrasByPlayer)

hook_mario_action(ACT_SPIRALMARIO_SPIRAL, act_spiralmario_spiral)
hook_mario_action(ACT_SPIRALMARIO_FLING, act_spiralmario_fling)

for i=0,(MAX_PLAYERS-1) do
    initExtrasByIndex(i)
end

However I didn't just make this post to gloat, I'm using it as a status update because I need some help again. The move largely works flawlessly to what I imagine it should look like, however there is an odd quirk in that when either custom action defined in my .lua file is executed via set_mario_action(), if that action happens to be occurring over quicksand, then Mario will instantly die no matter if he is actually touching the quicksand or not:

In times like this since I began this project, I've found my best port of call ruling out the spotty documentation was to just investigate the SM64 Decomp source over on GitHub since a lot of the .lua calls one can do have direct C equivalents in that source code. Sure enough, I found a method named sink_mario_in_quicksand() which occurs seemingly every frame during the per-tick call to execute_mario_action() unconditionally, which as far as I can tell MUST be my problem here. However, the solution is lost on me. The only way to avoid the execution of sink_mario_in_quicksand() is if my custom actions fall into certain action groups (ACT_GROUP_STATIONARY, ACT_GROUP_MOVING, etc.) however I see no way to set these groups in Lua nor do I think the consequenting reroutes to various action setter functions (e.g. mario_execute_airborne_action()) would do me any good since the contents of these rerouter methods would greatly disturb the execution of the action methods I have hooked in Lua.
 
Last edited:
You can set these flags via actionName = ACT_GROUP_X | allocate_mario_action(act_flags), although I don't think this is the solution.
sink_mario_in_quicksand() which occurs seemingly every frame during the per-tick call to execute_mario_action() unconditionally, which as far as I can tell MUST be my problem here
I don't believe this is the problem, as that function purely handles gfx values. I believe the problem here is mario's action is being set to ACT_QUICKSAND_DEATH. It can't be the function `check_for_instant_quicksand`, as that is ran in `mario_execute_cutscene_action`, which is only ran when your action is in `ACT_GROUP_CUTSCENE`. It's probably happening because of `mario_update_quicksand`. This is checked in moving, object(actions that allows you to grab bowser and stuff), and stationary actions. These are ran depending on your action group. ACT_GROUP_AIRBONE does not check for quicksand, and the action group this action is being set to is not that. I'd like another person's input on this, as this feels more like a bug than anything.

A pretty easy fix is to manually specify it being in the AIRBONE action:

Code:
ACT_SPIRALMARIO_SPIRAL = ACT_GROUP_AIRBORNE | allocate_mario_action(ACT_FLAG_AIR)
ACT_SPIRALMARIO_FLING = ACT_GROUP_AIRBORNE | allocate_mario_action(ACT_FLAG_AIR)

This sets the action group to airbone, which makes the `mario_update_quicksand` function not called. As stated above, I feel like this is more of a bug than anything, and I believe the intended behavior is it should be set the ACT_GROUP_AIRBONE when allocate_mario_action is called with ACT_FLAG_AIR.
Here's how the act group is set when running `allocate_mario_action`: `u32 actGroup = ((actFlags & ACT_GROUP_MASK) >> 6);`

While i'm no expert on bitwise operators, and couldn't tell you exactly what this is doing, to me it's trying to find the action group based off of the action flag, however take all that with a grain of salt, as I am not experienced with bitwise operators.

Anyways this was probably a bit in-depth for a very simple solution. Your mods look great and it looks like your past coding experience is coming in handy a lot! I tried it out, and the movement is super smooth and works great! Also, as a sidenote, it's great that you're looking in the source code already! That'll come in handy a lot when you start working on more advanced mods!
 
  • Like
Reactions: Spring E. Thing
You can set these flags via actionName = ACT_GROUP_X | allocate_mario_action(act_flags), although I don't think this is the solution.

I don't believe this is the problem, as that function purely handles gfx values. I believe the problem here is mario's action is being set to ACT_QUICKSAND_DEATH. It can't be the function `check_for_instant_quicksand`, as that is ran in `mario_execute_cutscene_action`, which is only ran when your action is in `ACT_GROUP_CUTSCENE`. It's probably happening because of `mario_update_quicksand`. This is checked in moving, object(actions that allows you to grab bowser and stuff), and stationary actions. These are ran depending on your action group. ACT_GROUP_AIRBONE does not check for quicksand, and the action group this action is being set to is not that. I'd like another person's input on this, as this feels more like a bug than anything.

A pretty easy fix is to manually specify it being in the AIRBONE action:

Code:
ACT_SPIRALMARIO_SPIRAL = ACT_GROUP_AIRBORNE | allocate_mario_action(ACT_FLAG_AIR)
ACT_SPIRALMARIO_FLING = ACT_GROUP_AIRBORNE | allocate_mario_action(ACT_FLAG_AIR)

This sets the action group to airbone, which makes the `mario_update_quicksand` function not called. As stated above, I feel like this is more of a bug than anything, and I believe the intended behavior is it should be set the ACT_GROUP_AIRBONE when allocate_mario_action is called with ACT_FLAG_AIR.
Here's how the act group is set when running `allocate_mario_action`: `u32 actGroup = ((actFlags & ACT_GROUP_MASK) >> 6);`

While i'm no expert on bitwise operators, and couldn't tell you exactly what this is doing, to me it's trying to find the action group based off of the action flag, however take all that with a grain of salt, as I am not experienced with bitwise operators.

Anyways this was probably a bit in-depth for a very simple solution. Your mods look great and it looks like your past coding experience is coming in handy a lot! I tried it out, and the movement is super smooth and works great! Also, as a sidenote, it's great that you're looking in the source code already! That'll come in handy a lot when you start working on more advanced mods!
Thanks for getting back so quick! I didn't even realize that allocate_mario_action() had a return value to use in a bitwise operation like this, all in all the solution is very elegant and readable as is often the benefit of bitwise stuff especially when interfacing with C. Truth be told, I don't fully understand bitwise operations myself even though I really ought to for work on a game like this. I totally get bits and bytes and can visualize that just fine, but something just gets lost in translation in my mind when bitwise operators modify those representations.
Anyway, that's besides the point, your advice worked perfectly and I thank you again. Your compliments to my work so far are also much appreciated! Personally given the 'finding my feet' nature of all this, the move I've developed actually isn't very useful, but at the least it's fun to mess around with I think : )
 
  • Like
Reactions: EmeraldLockdown
TL;DR How to get the direction vector and/or rotation of the camera?

Hey again guys, so I'm currently trying to start from scratch on a new experiment not because I didn't like the previous one or anything but I feel as though I learnt all I could from that concept and wanted to move on to something a little more involved. Anyway, I'm at a stage where I have practically no Lua code to show you for demonstrative purposes but I've been very stumped on something for about 2 hours now - How does one get the rotation and/or direction vector of the player's camera?

Here's a few of the things I've tried so far:
- Subtracting m.area.camera.pos from m.area.camera.focus, zero-ing the Y axis, then normalizing the result to get a direction vector for the camera, which to me seemed like it should work assuming the focus would always be the local player but clearly I misunderstood somehow since the results were all skewed and weird.

- Converting the m.area.camera.yaw int to a euler angle and going from there, however I never got that far because whilst the int as I expected was bound by the upper and lower limits of the int16 type, the area which was considered 0 for the value was oddly wide and it left the upper and lower bounds a few hundred units off from the maximum and minimum int16 values respectively which I thought was very odd.

- Doing the same as my first idea but this time with m.marioObj.header.gfx.cameraToObject and m.marioObj.header.gfx.pos respectively, however it lead to very similar results.


I'm starting to think there's just no way to do this rebuffed by the fact that the camera system for CoOp-DX is entirely divorced from the system in the vanilla SM64 game and so the Lua libraries just have no implementations to access any of this data. The reason I want the camera rotation and/or direction vector is so that I can set Mario's velocity according to the direction of the camera in addition to the direction and magnitude of the player's controller stick (already got that second part under wraps so don't worry about the stick).

Any ideas? Many thanks for the continued support!
 
Try `gLakituState` instead.
Thanks for the suggestion! It lead me to some curious findings.

Firstly, and rather disappointingly, the .yaw value of gLakituState actually has the exact same problem as the .yaw value of m.area.camera in that an abnormally wide range is considered a .yaw of 0 and the maximum/minimum .yaws are consequently several hundred units out from the maxmimum/minimum int16 values respectively,

Secondly, the .oldYaw value of gLakituState which i 100% expected to just be .yaw from a frame earlier was actually EXACTLY what I was looking for, full int16 range and all, however with it being 'old' it starts off with the completely wrong value until the camera is moved to some degree at which point it adjusts to be a frame off from reality which is a slight problem. This is by far the closest to an answer I have to this issue so far so I'll keep it in mind, I can maybe work around the downside on this one and make some assumptions about the player until the camera gets moved.

Thirdly, and perhaps most curiously, the .nextYaw value of gLakituState is ALWAYS 0, which is a letdown since I got my hopes up that it might just be an up-to-date version of .oldYaw.

I think given my options here that I'm best to go with .oldYaw and just assume the value to be a 0 UNTIL some sort of camera input occurs.

Will add a post to the thread upon my next issue, whatever it may be!

UPDATE: .oldYaw doesn't start off on the COMPLETELY wrong value, just one unit off from reality, so that's all fine by me given the degree of precision the value gives making the one unit difference in-practice completely unnoticable.
 
Hey guys, final update here I think.
I have new issues I want to work through with mod development, but they are all straying further and further from the thread topic. As such, I'm going to mark EmeraldLockdown's initial response as the 'answer' here and (hopefully) lock the thread in the process. IDK if that gives me the power to lock the thread as its creator, however personally I think I'm done here. Expect a new thread sometime soon about another aspect of needing modding help!
 
  • Like
Reactions: EmeraldLockdown
Status
Not open for further replies.

Users who are viewing this thread