diff --git a/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua new file mode 100644 index 0000000000..c466da3159 --- /dev/null +++ b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua @@ -0,0 +1,110 @@ +-- This will discuss how to create an option row in lua for use on a normal options screen. + +-- To try out this example, copy this file to Scripts/ and add the following +-- line to metrics.ini, on one of the options screens. Change the "1" to an +-- appropriate line name and make sure it's in the LineNames list for that +-- screen. +-- Line1="lua,FooMods()" + +-- When on the screen testing the example, flush the log and read it when +-- interacting with the option row. This example doesn't actually apply any +-- modifiers to the player, it just prints messages to the log file so you can +-- see what functions are being called. +-- Make sure you test this with Player 2, Player 1 can't interact with this +-- option row as part of the example. + +-- Comments explaining an element come before the element they explain. + +-- The function "FooMods" returns a table containing all the information the +-- option row handler needs to build the option row. +function FooMods() + return { + -- A string with the name of the row. This name will be localized using + -- the entry in the "OptionTitles" section of the language file. + Name= "Foo", + -- A boolean controlling whether the choice affects all players. + OneChoiceForAllPlayers= false, + -- A LayoutType enum value. "ShowAllInRow" shows all items in the row, + -- "ShowOneInRow" shows only the choice with focus is shown. + -- "ShowOneInRow" is forced if there are enough choices that they would go + -- off screen. + LayoutType= "ShowAllInRow", + -- A SelectType enum value. "SelectOne" allows only one choice to be + -- selected. "SelectMultiple" allows multiple to be selected. + -- "SelectNone" allows none to be selected. + SelectType= "SelectMultiple", + -- Optional function. If non-nil, this function must return a table of + -- PlayerNumbers that are allowed to use the row. + EnabledForPlayers= function(self) + Trace("FooMods:EnabledForPlayers() called.") + -- Leave out PLAYER_1 just for example. + return {PLAYER_2} + end, + -- A table of strings that are the names of choices. Choice names are not + -- localized. + Choices= {"a", "b", "c", "d"}, + -- Optional table. If non-nil, this table must contain a list of messages + -- this row should listen for. If one of the messages is recieved, the + -- row is reloaded and the EnabledForPlayers function is called if it is + -- non-nil. + ReloadRowMessages= {"ReloadFooMods"}, + -- LoadSelections should examine the player and figure out which options + -- on the row the player has selected. + -- self is the table returned by the original function used to create the + -- option row. (the table being created right now). + -- list is a table of bools, all initially false. Set them to true to + -- indicate which options are on. + -- pn is the PlayerNumber of the player the selections are for. + LoadSelections= function(self, list, pn) + Trace("FooMods:LoadSelections(" .. pn .. ")") + for i, choice in ipairs(self.Choices) do + -- Randomly set some to true just for an example. + if math.random(0, 1) == 1 then + Trace(choice .. " (" .. i .. ")" .. " set to true.") + list[i]= true + end + end + end, + -- SaveSelections should examin the list of what the player has selected + -- and apply the appropriate modifiers to the player. + -- Same args as LoadSelections + SaveSelections= function(self, list, pn) + Trace("FooMods:SaveSelections(" .. pn .. ")") + for i, choice in ipairs(self.Choices) do + if list[i] then + Trace(choice .. " (" .. i .. ")" .. " set to true.") + end + end + end, + -- Optional function. If non-nil, this function must take 3 parameters + -- (self, pn, choice), and return a bool. It is called when a player + -- selects an item in the row by pressing start. + -- self is the same as for LoadSelections. + -- pn is the PlayerNumber of the player that made the selection. + -- choice is the choice the player's cursor is on. + -- The return value should be true if the Choices table is changed. If it + -- is true, then LoadSelections will be called to update which choices are + -- underlined for each player. + -- This function is meant to provide a way for a menu to change the text of + -- its choices. If it returns true, LoadSelections will be called for each + -- player. If OneChoiceForAllPlayers is true, this function will be called + -- for each player, which means LoadSelections will be called twice for + -- each player. Well written code shouldn't have a problem with this. + NotifyOfSelection= function(self, pn, choice) + Trace("FooMods:NotifyOfSelection(" .. pn .. ", " .. choice .. ")") + -- Randomly decide whether to change, as an example. + local change= math.random(0, 3) + -- No change half the time, lengthen or clip strings the other half. + if change == 2 then + for i, choice in ipairs(self.Choices) do + self.Choices[i]= choice .. choice + end + elseif change == 3 then + for i, choice in ipairs(self.Choices) do + self.Choices[i]= choice:sub(1, 1) + end + end + return change >= 2 + end + } +end diff --git a/Themes/_fallback/Scripts/03 CustomSpeedMods.lua b/Themes/_fallback/Scripts/03 CustomSpeedMods.lua index 9ce066d813..7539c83165 100644 --- a/Themes/_fallback/Scripts/03 CustomSpeedMods.lua +++ b/Themes/_fallback/Scripts/03 CustomSpeedMods.lua @@ -243,10 +243,130 @@ function SpeedMods() state:SetPlayerOptions("ModsLevel_Preferred", self.Choices[1]) end } - setmetatable( t, t ) return t end +function ArbSpeedMods() + -- If players are allowed to join while this option row is active, problems will probably occur. + local ret= { + Name= "Speed", + LayoutType= "ShowAllInRow", + SelectType= "SelectMultiple", + OneChoiceForAllPlayers= false, + LoadSelections= function(self, list, pn) + -- The first values display the current status of the speed mod. + if pn == PLAYER_1 or self.NumPlayers == 1 then + list[1]= true + else + list[2]= true + end + end, + SaveSelections= function(self, list, pn) + local val= self.CurValues[pn] + local poptions= GAMESTATE:GetPlayerState(pn):GetPlayerOptions("ModsLevel_Preferred") + -- modify stage, song and current too so this will work in edit mode. + local stoptions= GAMESTATE:GetPlayerState(pn):GetPlayerOptions("ModsLevel_Stage") + local soptions= GAMESTATE:GetPlayerState(pn):GetPlayerOptions("ModsLevel_Song") + local coptions= GAMESTATE:GetPlayerState(pn):GetPlayerOptions("ModsLevel_Current") + if val.mode == "x" then + poptions:XMod(val.speed) + stoptions:XMod(val.speed) + soptions:XMod(val.speed) + coptions:XMod(val.speed) + elseif val.mode == "C" then + poptions:CMod(val.speed) + stoptions:CMod(val.speed) + soptions:CMod(val.speed) + coptions:CMod(val.speed) + else + poptions:MMod(val.speed) + stoptions:MMod(val.speed) + soptions:MMod(val.speed) + coptions:MMod(val.speed) + end + end, + NotifyOfSelection= function(self, pn, choice) + -- Adjust for the status elements + local real_choice= choice - self.NumPlayers + -- return true even though we didn't actually change anything so that the underlines will stay correct. + if real_choice < 1 then return true end + local val= self.CurValues[pn] + if real_choice < 5 then + local incs= {100, 25, -25, -100} + if val.mode == "x" then + val.speed= val.speed + (incs[real_choice] / 100) + else + val.speed= val.speed + incs[real_choice] + end + elseif real_choice == 5 then + if val.mode ~= "x" then + val.speed= val.speed / 100 + val.mode= "x" + end + elseif real_choice == 6 then + if val.mode == "x" then + val.speed= math.floor(val.speed * 100) + end + val.mode= "C" + elseif real_choice == 7 then + if val.mode == "x" then + val.speed= math.floor(val.speed * 100) + end + val.mode= "m" + end + self:GenChoices() + return true + end, + GenChoices= function(self) + -- We can't show different options to each player, so compromise by + -- only showing the xmod increments if one player is in that mode. + local show_x_incs= false + for pn, val in pairs(self.CurValues) do + if val.mode == "x" then + show_x_incs= true + end + end + if show_x_incs then + self.Choices= {"+1", "+.25", "-.25", "-1", "Xmod", "Cmod", "Mmod"} + else + self.Choices= {"+100", "+25", "-25", "-100", "Xmod", "Cmod", "Mmod"} + end + -- Insert the status element for P2 first so it will be second + for i, pn in ipairs({PLAYER_2, PLAYER_1}) do + local val= self.CurValues[pn] + if val then + if val.mode == "x" then + table.insert(self.Choices, 1, val.speed .. "x") + else + table.insert(self.Choices, 1, val.mode .. val.speed) + end + end + end + end, + CurValues= {}, -- for easy tracking of what speed the player wants + NumPlayers= 0 -- for ease when adjusting for the status elements. + } + for i, pn in ipairs(GAMESTATE:GetEnabledPlayers()) do + local poptions= GAMESTATE:GetPlayerState(pn):GetPlayerOptions("ModsLevel_Preferred") + local speed= nil + local mode= nil + if poptions:MaxScrollBPM() > 0 then + mode= "m" + speed= poptions:MaxScrollBPM() + elseif poptions:TimeSpacing() > 0 then + mode= "C" + speed= poptions:ScrollBPM() + else + mode= "x" + speed= poptions:ScrollSpeed() + end + ret.CurValues[pn]= {mode= mode, speed= speed} + ret.NumPlayers= ret.NumPlayers + 1 + end + ret:GenChoices() + return ret +end + --[[ CustomSpeedMods (c) 2013 StepMania team. diff --git a/Themes/_fallback/metrics.ini b/Themes/_fallback/metrics.ini index dad21231b0..c5e226edb1 100644 --- a/Themes/_fallback/metrics.ini +++ b/Themes/_fallback/metrics.ini @@ -3076,7 +3076,7 @@ PlayMusic=false TimerSeconds=30 # LineNames="1,2,3A,3B,4,5,6,R1,R2,7,8,9,10,11,12,13,14,16,17" -Line1="lua,SpeedMods()" +Line1="lua,ArbSpeedMods()" # Line1="list,Speed" Line2="list,Accel" Line3A="list,EffectsReceptor" diff --git a/Themes/default/metrics.ini b/Themes/default/metrics.ini index 1435f2789c..660b1dd4e8 100644 --- a/Themes/default/metrics.ini +++ b/Themes/default/metrics.ini @@ -2011,7 +2011,7 @@ SmallBanner6OffCommand= [ScreenEditOptions] LineNames="1,2,3,4,5,6,R1,R2,7,8,9,10,Attacks,11,12,13,14,15,16,SF" -Line1="lua,SpeedMods()" +Line1="lua,ArbSpeedMods()" LineSF="lua,OptionRowScreenFilter()" [StepsDisplayEdit] diff --git a/src/OptionRow.cpp b/src/OptionRow.cpp index 030cdfcd94..60f7fa1e66 100644 --- a/src/OptionRow.cpp +++ b/src/OptionRow.cpp @@ -148,7 +148,7 @@ void OptionRow::LoadExit() ChoicesChanged( RowType_Exit ); } -void OptionRow::ChoicesChanged( RowType type ) +void OptionRow::ChoicesChanged( RowType type, bool reset_focus ) { ASSERT_M( !m_pHand->m_Def.m_vsChoices.empty(), m_pHand->m_Def.m_sName + " has no choices" ); @@ -181,9 +181,13 @@ void OptionRow::ChoicesChanged( RowType type ) InitText( type ); - // When choices change, the old focus position is meaningless; reset it. - FOREACH_PlayerNumber( p ) - SetChoiceInRowWithFocus( p, 0 ); + // Lua can change the choices now, and when it does, we don't want to change focus. + if(reset_focus) + { + // When choices change, the old focus position is meaningless; reset it. + FOREACH_PlayerNumber( p ) + SetChoiceInRowWithFocus( p, 0 ); + } m_textTitle->SetText( GetRowTitle() ); } @@ -705,6 +709,7 @@ void OptionRow::SetOneSelection( PlayerNumber pn, int iChoice ) FOREACH( bool, vb, b ) *b = false; vb[iChoice] = true; + NotifyHandlerOfSelection(pn, iChoice); } void OptionRow::SetOneSharedSelection( int iChoice ) @@ -788,11 +793,31 @@ OptionRowDefinition &OptionRow::GetRowDef() return m_pHand->m_Def; } -void OptionRow::SetSelected( PlayerNumber pn, int iChoice, bool b ) +bool OptionRow::SetSelected( PlayerNumber pn, int iChoice, bool b ) { if( m_pHand->m_Def.m_bOneChoiceForAllPlayers ) pn = PLAYER_1; m_vbSelected[pn][iChoice] = b; + return NotifyHandlerOfSelection(pn, iChoice); +} + +bool OptionRow::NotifyHandlerOfSelection(PlayerNumber pn, int choice) +{ + bool changed= m_pHand->NotifyOfSelection(pn, choice); + if(changed) + { + ChoicesChanged(m_RowType, false); + vector vpns; + FOREACH_HumanPlayer( p ) + vpns.push_back( p ); + ImportOptions(vpns); + FOREACH_PlayerNumber(p) + { + PositionUnderlines(p); + } + UpdateEnabledDisabled(); + } + return changed; } void OptionRow::SetExitText( RString sExitText ) diff --git a/src/OptionRow.h b/src/OptionRow.h index c7e2817c95..7a2c825bca 100644 --- a/src/OptionRow.h +++ b/src/OptionRow.h @@ -76,7 +76,7 @@ public: RString GetRowTitle() const; - void ChoicesChanged( RowType type ); + void ChoicesChanged( RowType type, bool reset_focus= true ); void PositionUnderlines( PlayerNumber pn ); void PositionIcons( PlayerNumber pn ); void UpdateText( PlayerNumber pn ); @@ -96,7 +96,10 @@ public: void ResetFocusFromSelection( PlayerNumber pn ); bool GetSelected( PlayerNumber pn, int iChoice ) const; - void SetSelected( PlayerNumber pn, int iChoice, bool b ); + // SetSelected returns true if the choices changed because of setting. + bool SetSelected( PlayerNumber pn, int iChoice, bool b ); + + bool NotifyHandlerOfSelection(PlayerNumber pn, int choice); const OptionRowDefinition &GetRowDef() const; OptionRowDefinition &GetRowDef(); diff --git a/src/OptionRowHandler.cpp b/src/OptionRowHandler.cpp index 01f0d32fc9..646927c212 100644 --- a/src/OptionRowHandler.cpp +++ b/src/OptionRowHandler.cpp @@ -834,7 +834,9 @@ public: lua_call( L, 1, 1 ); // call function with 1 argument and 1 result if( !lua_istable(L, -1) ) - RageException::Throw( "\"EnabledForPlayers\" did not return a table." ); + { + LOG->Warn("LUA_ERROR: \"EnabledForPlayers\" did not return a table." ); + } m_Def.m_vEnabledForPlayers.clear(); // and fill in with supplied PlayerNumbers below @@ -842,7 +844,7 @@ public: while( lua_next(L, -2) != 0 ) { // `key' is at index -2 and `value' at index -1 - PlayerNumber pn = (PlayerNumber)luaL_checkint( L, -1 ); + PlayerNumber pn = Enum::Check(L, -1); m_Def.m_vEnabledForPlayers.insert( pn ); @@ -869,7 +871,9 @@ public: m_pLuaTable->SetFromExpression( sLuaFunction ); if( m_pLuaTable->GetLuaType() != LUA_TTABLE ) - RageException::Throw( "Result of \"%s\" is not a table.", sLuaFunction.c_str() ); + { + LOG->Warn("LUA_ERROR: Result of \"%s\" is not a table.", sLuaFunction.c_str()); + } m_pLuaTable->PushSelf( L ); @@ -877,25 +881,29 @@ public: lua_gettable( L, -2 ); const char *pStr = lua_tostring( L, -1 ); if( pStr == NULL ) - RageException::Throw( "\"%s\" \"Name\" entry is not a string.", sLuaFunction.c_str() ); + { + LOG->Warn("LUA_ERROR: \"%s\" \"Name\" entry is not a string.", sLuaFunction.c_str()); + } m_Def.m_sName = pStr; lua_pop( L, 1 ); lua_pushstring( L, "OneChoiceForAllPlayers" ); lua_gettable( L, -2 ); - m_Def.m_bOneChoiceForAllPlayers = !!lua_toboolean( L, -1 ); + m_Def.m_bOneChoiceForAllPlayers = lua_toboolean( L, -1 ); lua_pop( L, 1 ); lua_pushstring( L, "ExportOnChange" ); lua_gettable( L, -2 ); - m_Def.m_bExportOnChange = !!lua_toboolean( L, -1 ); + m_Def.m_bExportOnChange = lua_toboolean( L, -1 ); lua_pop( L, 1 ); lua_pushstring( L, "LayoutType" ); lua_gettable( L, -2 ); pStr = lua_tostring( L, -1 ); if( pStr == NULL ) - RageException::Throw( "\"%s\" \"LayoutType\" entry is not a string.", sLuaFunction.c_str() ); + { + LOG->Warn("LUA_ERROR: \"%s\" \"LayoutType\" entry is not a string.", sLuaFunction.c_str()); + } m_Def.m_layoutType = StringToLayoutType( pStr ); ASSERT( m_Def.m_layoutType != LayoutType_Invalid ); lua_pop( L, 1 ); @@ -904,7 +912,9 @@ public: lua_gettable( L, -2 ); pStr = lua_tostring( L, -1 ); if( pStr == NULL ) - RageException::Throw( "\"%s\" \"SelectType\" entry is not a string.", sLuaFunction.c_str() ); + { + LOG->Warn("LUA_ERROR: \"%s\" \"SelectType\" entry is not a string.", sLuaFunction.c_str()); + } m_Def.m_selectType = StringToSelectType( pStr ); ASSERT( m_Def.m_selectType != SelectType_Invalid ); lua_pop( L, 1 ); @@ -913,7 +923,9 @@ public: lua_pushstring( L, "Choices" ); lua_gettable( L, -2 ); if( !lua_istable( L, -1 ) ) - RageException::Throw( "\"%s\" \"Choices\" is not a table.", sLuaFunction.c_str() ); + { + LOG->Warn("LUA_ERROR: \"%s\" \"Choices\" is not a table.", sLuaFunction.c_str()); + } lua_pushnil( L ); while( lua_next(L, -2) != 0 ) @@ -921,8 +933,10 @@ public: // `key' is at index -2 and `value' at index -1 const char *pValue = lua_tostring( L, -1 ); if( pValue == NULL ) - RageException::Throw( "\"%s\" Column entry is not a string.", sLuaFunction.c_str() ); -// LOG->Trace( "'%s'", pValue); + { + LOG->Warn("LUA_ERROR: \"%s\" Column entry is not a string.", sLuaFunction.c_str()); + } + //LOG->Trace( "choice: '%s'", pValue); m_Def.m_vsChoices.push_back( pValue ); @@ -935,7 +949,9 @@ public: lua_pushstring( L, "EnabledForPlayers" ); lua_gettable( L, -2 ); if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) - RageException::Throw( "\"%s\" \"EnabledForPlayers\" is not a table.", sLuaFunction.c_str() ); + { + LOG->Warn("LUA_ERROR: \"%s\" \"EnabledForPlayers\" is not a function.", sLuaFunction.c_str()); + } m_EnabledForPlayersFunc.SetFromStack( L ); SetEnabledForPlayers(); @@ -953,8 +969,10 @@ public: // `key' is at index -2 and `value' at index -1 const char *pValue = lua_tostring( L, -1 ); if( pValue == NULL ) - RageException::Throw( "\"%s\" Column entry is not a string.", sLuaFunction.c_str() ); - LOG->Trace( "Found ReloadRowMessage '%s'", pValue); + { + LOG->Warn("LUA_ERROR: \"%s\" Column entry is not a string.", sLuaFunction.c_str()); + } + //LOG->Trace( "Found ReloadRowMessage '%s'", pValue); m_vsReloadRowMessages.push_back( pValue ); @@ -963,15 +981,6 @@ public: } lua_pop( L, 1 ); // pop ReloadRowMessages table - // Look for "ExportOnChange" value. - lua_pushstring( L, "ExportOnChange" ); - lua_gettable( L, -2 ); - if( !lua_isnil( L, -1 ) ) - { - m_Def.m_bExportOnChange = !!MyLua_checkboolean( L, -1 ); - } - lua_pop( L, 1 ); // pop ExportOnChange value - lua_pop( L, 1 ); // pop main table ASSERT( lua_gettop(L) == 0 ); @@ -1013,7 +1022,9 @@ public: lua_pushstring( L, "LoadSelections" ); lua_gettable( L, -2 ); if( !lua_isfunction( L, -1 ) ) - RageException::Throw( "\"%s\" \"LoadSelections\" entry is not a function.", m_Def.m_sName.c_str() ); + { + LOG->Warn("LUA_ERROR: \"%s\" \"LoadSelections\" entry is not a function.", m_Def.m_sName.c_str()); + } // Argument 1 (self): m_pLuaTable->PushSelf( L ); @@ -1067,7 +1078,9 @@ public: lua_pushstring( L, "SaveSelections" ); lua_gettable( L, -2 ); if( !lua_isfunction( L, -1 ) ) - RageException::Throw( "\"%s\" \"SaveSelections\" entry is not a function.", m_Def.m_sName.c_str() ); + { + LOG->Warn("LUA_ERROR: \"%s\" \"SaveSelections\" entry is not a function.", m_Def.m_sName.c_str()); + } // Argument 1 (self): m_pLuaTable->PushSelf( L ); @@ -1094,6 +1107,54 @@ public: // XXX: allow specifying the mask return 0; } + virtual bool NotifyOfSelection(PlayerNumber pn, int choice) + { + Lua *L= LUA->Get(); + m_pLuaTable->PushSelf(L); + + lua_pushstring(L, "NotifyOfSelection"); + lua_gettable(L, -2); + bool changed= false; + if(lua_isfunction(L, -1)) + { + m_pLuaTable->PushSelf(L); + LuaHelpers::Push(L, pn); + // Convert choice to a lua index so it matches up with the Choices table. + lua_pushinteger(L, choice+1); + lua_call(L, 3, 1); + if(lua_toboolean(L, -1)) + { + lua_pop(L, 1); + changed= true; + m_Def.m_vsChoices.clear(); + // Iterate over the "Choices" table. + lua_pushstring( L, "Choices" ); + lua_gettable( L, -2 ); + if(!lua_istable(L, -1)) + { + LOG->Warn("\"%s\" \"Choices\" is not a table.", m_Def.m_sName.c_str()); + } + lua_pushnil( L ); + while( lua_next(L, -2) != 0 ) + { + // `key' is at index -2 and `value' at index -1 + const char *pValue = lua_tostring( L, -1 ); + if(pValue == NULL) + { + LOG->Warn("\"%s\" Column entry is not a string.", m_Def.m_sName.c_str()); + } + //LOG->Trace( "choice: '%s'", pValue); + + m_Def.m_vsChoices.push_back( pValue ); + + lua_pop( L, 1 ); // removes `value'; keeps `key' for next iteration + } + } + } + lua_settop(L, 0); // Release has an assert that forces a clear stack. + LUA->Release(L); + return changed; + } }; class OptionRowHandlerConfig : public OptionRowHandler diff --git a/src/OptionRowHandler.h b/src/OptionRowHandler.h index 232ccafee6..4f438fc699 100644 --- a/src/OptionRowHandler.h +++ b/src/OptionRowHandler.h @@ -178,6 +178,8 @@ public: virtual int ExportOption( const vector &, const vector vbSelected[NUM_PLAYERS] ) const { return 0; } virtual void GetIconTextAndGameCommand( int iFirstSelection, RString &sIconTextOut, GameCommand &gcOut ) const; virtual RString GetScreen( int /* iChoice */ ) const { return RString(); } + // Exists so that a lua function can act on the selection. Returns true if the choices should be reloaded. + virtual bool NotifyOfSelection(PlayerNumber pn, int choice) { return false; } }; /** @brief Utilities for the OptionRowHandlers. */ diff --git a/src/ScreenOptions.cpp b/src/ScreenOptions.cpp index e32b21b7dc..a368adb77e 100644 --- a/src/ScreenOptions.cpp +++ b/src/ScreenOptions.cpp @@ -895,7 +895,11 @@ void ScreenOptions::ProcessMenuStart( const InputEventPlus &input ) { int iChoiceInRow = row.GetChoiceInRowWithFocus(pn); bool bSelected = !row.GetSelected( pn, iChoiceInRow ); - row.SetSelected( pn, iChoiceInRow, bSelected ); + bool changed= row.SetSelected( pn, iChoiceInRow, bSelected ); + if(changed) + { + AfterChangeValueOrRow(pn); + } if( bSelected ) m_SoundToggleOn.Play();