Skip to content

Added SMODS.GameState API#1307

Open
AllUniversal wants to merge 46 commits into
Steamodded:mainfrom
AllUniversal:smods-game-states
Open

Added SMODS.GameState API#1307
AllUniversal wants to merge 46 commits into
Steamodded:mainfrom
AllUniversal:smods-game-states

Conversation

@AllUniversal
Copy link
Copy Markdown
Contributor

@AllUniversal AllUniversal commented Apr 4, 2026

+/*Title, I moved this over from #1055. Functionally everything seems to be working like in Vanilla, but there might be slight visual or timing differences, let me know if you spot anything.

In short, the API lets you create new game states.

In less short, it lets you create game states, and the following vanilla states have been replaced with SMODS states;
G.STATES.BLIND_SELECT, G.STATES.ROUND_EVAL and G.STATES.SHOP, plus a new state G.STATES.BLIND for during a blind, plus the two SMODS states BOOSTER_OPENED and REDEEM_VOUCHER.
The game state is still primarily tracked using G.STATE, but there's also an SMODS.STATE, which only updates with SMODS.GameStates.
-> This is useful because of how states enter and exit.

I'm gonna add some more details on the whole API later, ask away if you have questions.

EDIT 1: Blinds got a new field blind_types, it's a map of types, e.g. {Boss = true, Big = true} + I added/changed some SMODS functions regarding blinds, most notably SMODS.get_new_blind(), which accounts for the new blind_types field and replaces get_new_boss(). Moved this over to #1319.

EDIT 2: Currently I overrode G.FUNCS.use_card() and G.FUNCS.end_consumable() entirely. Both (and especially the latter) could probably be done with lovely patches instead. (Probably would be better for compatibility too) Nothing to see here, move along!

EDIT 3: Alright so it's fundamentally working, but I still want to add some functionality, specifically an improved ability to hold a state when entering a different one. (This is technically already implemented, but it requires some more work to really transition smoothly) Done.

Additional Info:

  • I didn't modify api's or I've made a PR to the wiki repo.
  • I didn't modify api's or I've updated lsp definitions.
  • I didn't make new lovely files or all new lovely files have appropriate priority.

Comment thread src/game_object.lua Outdated
Copy link
Copy Markdown
Member

@Aurelius7309 Aurelius7309 left a comment

Choose a reason for hiding this comment

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

This is largely bloated by overrides of functions with no structural changes. Overrides should be reserved for large changes to the code structure of a function; the changes made here don't warrant breaking every mod that patches into these functions (of course patching doesn't guarantee them not breaking, but it reduces breakage to the code that was actually changed instead of the whole function).

Comment thread src/overrides.lua Outdated
Comment thread src/overrides.lua Outdated
Comment thread src/overrides.lua Outdated
…ng and returning to multiple different Shops)

*Title
*/-Title, `SMODS.clear_states()` was kinda unnecessary, as states should clean up after themselves anyway.
+Title, hopefully that's all... (..hyuck)
…d` behaviour a little

+/*Title, the new functions are mostly for internal use, and `from_hold` may still not be fully implemented for all states, especially `BLIND`.
*Title, better name for what it does + fixes an oversight.
*Title, realized this could be simpler
+/*Title, this replaces straight `SMODS.enter_state()` calls to allow queueing states instead.
-> Base SMODS only queues the next state once it would've called `SMODS.enter_state()`, and only if no states are currently queued, as if there is a state queued, it's best if the mod that queued it also handles advancing the state queue after.
*Title, woopsie
@AllUniversal AllUniversal marked this pull request as draft April 6, 2026 23:04
@AllUniversal
Copy link
Copy Markdown
Contributor Author

Did some testing regarding the blind_types and other SMODS.Blind changes and stuff isn't quite working there + I still need to implement some stuff regarding the new SMODS.STATES.BLIND state, so I converted this PR to Draft for now.

…ODS.STATE` related stuff

+Title, saving and loading are yet untested though.
-> Currently no state stores unserializable data, in fact only `BLIND` stores any data at all; The hand and discarded cards, serialized by sort_id.
+/*Title, I also added a new `SMODS.GameState` function; `on_load`.
-> `SHOP` uses this to create and load `G.shop`.
+/*Title
@AllUniversal AllUniversal marked this pull request as ready for review April 7, 2026 23:02
@AllUniversal
Copy link
Copy Markdown
Contributor Author

AllUniversal commented Apr 7, 2026

I have now added everything I wanted to add, thus I marked the PR as ready again! :)
If issues (especially regarding visuals / timings) come up or you have suggestions for other additions, please let me know.

As for LSP defs: ermmm

*Title, changes are yet untested!
! The showdown check isn't needed anymore because get_new_boss() already changes it's requested blind type according to the showdown ante.
*Title, updated `get_new_boss()` to use `SMODS.is_showdown_ante()`.
AllUniversal added a commit to AllUniversal/smods that referenced this pull request Apr 8, 2026
*/+Title, realized this best be a separate PR too.
@AllUniversal AllUniversal mentioned this pull request Apr 8, 2026
3 tasks
@AllUniversal
Copy link
Copy Markdown
Contributor Author

I realized that G.STATES.PLAY_TAROT probably had to be overridden as well, so I added SMODS.STATES.USE_CONSUMABLE now. But timings and visuals are currently a little different from Vanilla I think.

Also, I tried adding a new state to test the API;

local test_hook = G.FUNCS.toggle_shop
function G.FUNCS.toggle_shop(e)
    if #SMODS.state_queue == 0 then
		SMODS.queue_state(SMODS.STATES.STATE1)
	end
	return test_hook(e)
end

function G.FUNCS.toggle_state1(e)
    if #SMODS.state_queue == 0 then
		SMODS.queue_state(SMODS.STATES.BLIND_SELECT)
	end
	SMODS.advance_state_queue()
end

SMODS.STATES.STATE1 = "state1"

SMODS.GameState {
    key = SMODS.STATES.STATE1,
    on_enter = function (self, args)
        args = args or {}
        if args.force_refresh then
            self:on_exit()
        elseif args.from_hold then
            if G.state1 then
                G.state1.alignment.offset.y = G.state1.alignment.offset.py
                G.state1.alignment.offset.py = nil
                G.E_MANAGER:add_event(Event({
                    trigger = "after",
                    delay = 0.3,
                    func = function ()
                        play_sound('cancel')
                        return true
                    end
                }))
            else
                args.force_refresh = true
                self:on_enter(args)
            end
            return
        end
        G.E_MANAGER:add_event(Event({
            trigger = "immediate",
            func = function()
                stop_use()
                G.E_MANAGER:add_event(Event({ func = function() save_run(); return true end}))
                G.CONTROLLER.interrupt.focus = true
                G.E_MANAGER:add_event(Event({ func = function()
                    G.E_MANAGER:add_event(Event({
                        trigger = 'immediate',
                        func = function()
                            play_sound('cancel')
                            G.state1 = UIBox{
                                definition = {n=G.UIT.ROOT, config = {align = 'tm',minw = G.hand.T.w, r = 0.15, colour = G.C.CLEAR}, nodes={
                                    {n=G.UIT.R,config={id = 'continue_button', align = "cm", minw = 2.8, minh = 1.5, r=0.15,colour = G.C.RED, one_press = true, button = 'toggle_state1', hover = true,shadow = true}, nodes = {
                                        {n=G.UIT.R, config={align = "cm", padding = 0.07, focus_args = {button = 'y', orientation = 'cr'}, func = 'set_button_pip'}, nodes={
                                            {n=G.UIT.R, config={align = "cm", maxw = 1.3}, nodes={
                                            {n=G.UIT.T, config={text = localize('b_next_round_1'), scale = 0.4, colour = G.C.WHITE, shadow = true}}
                                            }},
                                            {n=G.UIT.R, config={align = "cm", maxw = 1.3}, nodes={
                                            {n=G.UIT.T, config={text = localize('b_next_round_2'), scale = 0.4, colour = G.C.WHITE, shadow = true}}
                                            }}   
                                        }},
                                    }}
                                }},
                                config = {align="bmi", offset = {x=0,y=G.ROOM.T.y + 29},major = G.hand, bond = 'Weak'}
                            }
                            G.state1.alignment.offset.y = 0.8-(G.hand.T.y - G.jokers.T.y) + G.state1.T.h * 2
                            G.ROOM.jiggle = G.ROOM.jiggle + 3
                            G.state1.alignment.offset.x = 0
                            G.CONTROLLER.lock_input = false
                            return true
                        end
                    }))
                    return true
                end}))
                return true
            end
        }))
    end,
    on_exit = function (self, args)
        args = args or {}
        if args.from_hold then
            if G.state1 and not G.state1.alignment.offset.py then
                G.state1.alignment.offset.py = G.state1.alignment.offset.y
                G.state1.alignment.offset.y = G.ROOM.T.y + 39
            end
            return
        end
        G.E_MANAGER:add_event(Event({
            trigger = 'before', delay = 0.2,
            func = function()
                G.state1.alignment.offset.y = -10
                G.state1.alignment.offset.y = 40
                G.state1.alignment.offset.x = 0
                return true
        end}))
        G.E_MANAGER:add_event(Event({
            trigger = "immediate",
            func = function ()
                G.state1:remove()
                G.state1 = nil
                return true
            end
        }))
    end,
}

This is largely based on the SMODS.STATES.BLIND_SELECT state and works as-is, interjecting itself between the Shop and the Blind Select screen. I'm still thinking about best practice for cross mod compatibility; The above method is weak, in the sense that it fails to interject the new state at all if another state did so first. If you'd use SMODS.queue_state unconditionally, you'd guarantee that your state is queued after the Shop, which for 99% of modded States I believe would be the right way to do it. But its limited in regards to completely changing the order of states. (Though depending on the use workarounds for compatibility probably exist).
Still, I might take another look at how to cleanly parallel queue multiple states after a specific state ends.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants