From 045e9edcdddfde82089c6bd5079d9bf3c6729aec Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Thu, 19 Jun 2014 06:41:18 -0600 Subject: [PATCH 1/5] Added ArbSpeedMods function for a better way of setting speed modifiers. Added NotifyHandlerOfSelection to OptionRowHandler to support this. Changed exceptions thrown by OptionRowHandlerLua into log warnings because crashing out is not helpful to themers. Fixed SetEnabledForPlayers in OptionRowHandlerLua to correctly read the PlayerNumber enum. Removed silly git add Docs/Themerdocs/Examples/OptionRowHandlerLua.lua and second check for ExportOnChange. --- .../Examples/OptionRowHandlerLua.lua | 110 ++++++++++++++++ .../_fallback/Scripts/03 CustomSpeedMods.lua | 122 +++++++++++++++++- Themes/_fallback/metrics.ini | 2 +- Themes/default/metrics.ini | 2 +- src/OptionRow.cpp | 35 ++++- src/OptionRow.h | 7 +- src/OptionRowHandler.cpp | 111 ++++++++++++---- src/OptionRowHandler.h | 2 + src/ScreenOptions.cpp | 6 +- 9 files changed, 361 insertions(+), 36 deletions(-) create mode 100644 Docs/Themerdocs/Examples/OptionRowHandlerLua.lua 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(); From c02a17f3938cb46f9f257ad74fb7ac49f29737c4 Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Thu, 19 Jun 2014 21:02:35 -0600 Subject: [PATCH 2/5] Revised documentation example for OptionRowHandlerLua to make it slightly more clear. --- .../Examples/OptionRowHandlerLua.lua | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua index c466da3159..d4f8afc974 100644 --- a/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua +++ b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua @@ -6,12 +6,12 @@ -- screen. -- Line1="lua,FooMods()" +-- Make sure you test this with Player 2, Player 1 can't interact with this +-- option row as part of the example. -- 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. @@ -22,32 +22,46 @@ function FooMods() -- 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 boolean controlling whether SaveSelections is called after every + -- change. If this is true, SaveSelections will be called every time + -- the player moves the cursor on the row. + ExportOnChange= 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. + -- A row that can't be used by one player can be confusing for the players + -- so consider carefully before using this. 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. + -- 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 @@ -65,9 +79,10 @@ function FooMods() end end end, - -- SaveSelections should examin the list of what the player has selected + + -- SaveSelections should examine the list of what the player has selected -- and apply the appropriate modifiers to the player. - -- Same args as LoadSelections + -- Same args as LoadSelections. SaveSelections= function(self, list, pn) Trace("FooMods:SaveSelections(" .. pn .. ")") for i, choice in ipairs(self.Choices) do @@ -76,6 +91,7 @@ function FooMods() 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. @@ -95,6 +111,11 @@ function FooMods() -- 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 + Trace("Not changing choices.") + else + Trace("Changing choices.") + end if change == 2 then for i, choice in ipairs(self.Choices) do self.Choices[i]= choice .. choice @@ -104,6 +125,14 @@ function FooMods() self.Choices[i]= choice:sub(1, 1) end end + -- Don't actually broadcast a reload message from here, do it from some + -- other place that actually has a reason to trigger a reload. This is + -- just here so that nothing needs to be added to a screen's lua files + -- for this example. + if math.random(0, 5) == 0 then + Trace("Broadcasting reload message.") + MESSAGEMAN:Broadcast("ReloadFooMods") + end return change >= 2 end } From de8799ebdc42794a4bec04bfa5499f7385338a64 Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Fri, 20 Jun 2014 22:48:00 -0600 Subject: [PATCH 3/5] Renamed ArbSpeedMods to ArbitrarySpeedMods to avoid the wrath of the OpenGL Architecture Review Board. Added option rows for controlling the size of the increments used by ArbitrarySpeedMods. Simplified logic in ASM and added rounding to avoid excessive precision. Removed pointless setmetatable calls from various lua option rows. --- Themes/_fallback/Languages/en.ini | 4 + Themes/_fallback/Scripts/00 init.lua | 9 + Themes/_fallback/Scripts/02 OptionsMenu.lua | 2 - .../_fallback/Scripts/03 CustomSpeedMods.lua | 157 ++++++++++++++---- .../Scripts/03 ThemeAndGamePrefs.lua | 2 - Themes/_fallback/metrics.ini | 6 +- Themes/default/metrics.ini | 2 +- 7 files changed, 143 insertions(+), 39 deletions(-) diff --git a/Themes/_fallback/Languages/en.ini b/Themes/_fallback/Languages/en.ini index b1ed7adaad..6ca37d22e9 100644 --- a/Themes/_fallback/Languages/en.ini +++ b/Themes/_fallback/Languages/en.ini @@ -478,6 +478,8 @@ SoundResampleQuality=Advanced: Select the resampling quality to use. SoundVolume=Adjust the volume level during gameplay. SoundVolumeAttract=Adjust the volume level during attract mode. Speed=Speed +Speed Increment=Size of the smaller increments used to adjust the speed mod. +Speed Multiple=Multiplied by Speed Increment to calculate the size of the large increments used to adjust the speed mod. StepMania Credits=The StepMania Development Team. Steps=Steps StretchBackgrounds=Toggle the stretching of backgrounds. @@ -1099,6 +1101,8 @@ SoundResampleQuality=Resampling Quality SoundVolume=Sound Volume SoundVolumeAttract=Attract Volume Speed=Speed +Speed Increment=Speed Increment +Speed Multiple=Speed Multiple Step Author=Step Author Steps=Steps Stream=Stream diff --git a/Themes/_fallback/Scripts/00 init.lua b/Themes/_fallback/Scripts/00 init.lua index 1f4d2950bc..64062f0efb 100644 --- a/Themes/_fallback/Scripts/00 init.lua +++ b/Themes/_fallback/Scripts/00 init.lua @@ -34,6 +34,15 @@ function string:find_last(text) end end +-- Round to nearest integer. +function math.round(n) + if n > 0 then + return math.floor(n+0.5) + else + return math.ceil(n-0.5) + end +end + -- (c) 2006 Glenn Maynard -- All rights reserved. -- diff --git a/Themes/_fallback/Scripts/02 OptionsMenu.lua b/Themes/_fallback/Scripts/02 OptionsMenu.lua index 0c3dfd9e9d..e6742c7ff5 100644 --- a/Themes/_fallback/Scripts/02 OptionsMenu.lua +++ b/Themes/_fallback/Scripts/02 OptionsMenu.lua @@ -91,7 +91,6 @@ function OptionsRandomJukebox() GAMESTATE:SetJukeboxUsesModifiers(val) end } - setmetatable( t, t ) return t end @@ -134,7 +133,6 @@ function OptionsWeight() end end, } - setmetatable(t, t) return t end diff --git a/Themes/_fallback/Scripts/03 CustomSpeedMods.lua b/Themes/_fallback/Scripts/03 CustomSpeedMods.lua index 7539c83165..43824914c3 100644 --- a/Themes/_fallback/Scripts/03 CustomSpeedMods.lua +++ b/Themes/_fallback/Scripts/03 CustomSpeedMods.lua @@ -246,8 +246,105 @@ function SpeedMods() return t end -function ArbSpeedMods() +local default_speed_increment= 25 +local default_speed_inc_multiple= 4 + +local function get_speed_increment() + local increment= default_speed_increment + if ReadGamePrefFromFile("SpeedIncrement") then + increment= tonumber(GetGamePref("SpeedIncrement")) or default_speed_increment + else + WriteGamePrefToFile("SpeedIncrement", increment) + end + return increment +end + +local function get_speed_multiple() + local multiple= default_speed_inc_multiple + if ReadGamePrefFromFile("SpeedMultiple") then + multiple= tonumber(GetGamePref("SpeedMultiple")) or default_speed_inc_multiple + else + WriteGamePrefToFile("SpeedMultiple", multiple) + end + return multiple +end + +function SpeedModIncSize() + -- An option row for controlling the size of the increment used by + -- ArbitrarySpeedMods. + local increment= get_speed_increment() + local ret= { + Name= "Speed Increment", + LayoutType= "ShowAllInRow", + SelectType= "SelectMultiple", + OneChoiceForAllPlayers= true, + LoadSelections= function(self, list, pn) + -- The first value is the status element, only it should be true. + list[1]= true + end, + SaveSelections= function(self, list, pn) + WriteGamePrefToFile("SpeedIncrement", increment) + end, + NotifyOfSelection= function(self, pn, choice) + -- return true even though we didn't actually change anything so that + -- the underlines will stay correct. + if choice == 1 then return true end + local incs= {10, 1, -1, -10} + local new_val= increment + incs[choice-1] + if new_val > 0 then + increment= new_val + end + self:GenChoices() + return true + end, + GenChoices= function(self) + self.Choices= {tostring(increment), "+10", "+1", "-1", "-10"} + end + } + ret:GenChoices() + return ret +end + +function SpeedModIncMultiple() + -- An option row for controlling the size of the increment used by + -- ArbitrarySpeedMods. + local multiple= get_speed_multiple() + local ret= { + Name= "Speed Multiple", + LayoutType= "ShowAllInRow", + SelectType= "SelectMultiple", + OneChoiceForAllPlayers= true, + LoadSelections= function(self, list, pn) + -- The first value is the status element, only it should be true. + list[1]= true + end, + SaveSelections= function(self, list, pn) + WriteGamePrefToFile("SpeedMultiple", multiple) + end, + NotifyOfSelection= function(self, pn, choice) + -- return true even though we didn't actually change anything so that + -- the underlines will stay correct. + if choice == 1 then return true end + local incs= {5, 1, -1, -5} + local new_val= multiple + incs[choice-1] + if new_val > 0 then + multiple= new_val + end + self:GenChoices() + return true + end, + GenChoices= function(self) + self.Choices= {tostring(multiple), "+5", "+1", "-1", "-5"} + end + } + ret:GenChoices() + return ret +end + +function ArbitrarySpeedMods() -- If players are allowed to join while this option row is active, problems will probably occur. + local increment= get_speed_increment() + local multiple= get_speed_multiple() local ret= { Name= "Speed", LayoutType= "ShowAllInRow", @@ -269,10 +366,11 @@ function ArbSpeedMods() 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) + local speed= val.speed / 100 + poptions:XMod(speed) + stoptions:XMod(speed) + soptions:XMod(speed) + coptions:XMod(speed) elseif val.mode == "C" then poptions:CMod(val.speed) stoptions:CMod(val.speed) @@ -288,31 +386,19 @@ function ArbSpeedMods() 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. + -- 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] + local big_inc= increment * multiple + local incs= {big_inc, increment, -increment, -big_inc} + local new_val= val.speed + incs[real_choice] + if new_val > 0 then + val.speed= math.round(new_val) 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" + elseif real_choice >= 5 then + val.mode= ({"x", "C", "m"})[real_choice - 4] end self:GenChoices() return true @@ -326,17 +412,24 @@ function ArbSpeedMods() show_x_incs= true end end + local big_inc= increment * multiple + local small_inc= increment if show_x_incs then - self.Choices= {"+1", "+.25", "-.25", "-1", "Xmod", "Cmod", "Mmod"} + big_inc= tostring(big_inc / 100) + small_inc= tostring(small_inc / 100) else - self.Choices= {"+100", "+25", "-25", "-100", "Xmod", "Cmod", "Mmod"} + big_inc= tostring(big_inc) + small_inc= tostring(small_inc) end + self.Choices= { + "+" .. big_inc, "+" .. small_inc, "-" .. small_inc, "-" .. big_inc, + "Xmod", "Cmod", "Mmod"} -- 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") + table.insert(self.Choices, 1, (val.speed/100) .. "x") else table.insert(self.Choices, 1, val.mode .. val.speed) end @@ -352,13 +445,13 @@ function ArbSpeedMods() local mode= nil if poptions:MaxScrollBPM() > 0 then mode= "m" - speed= poptions:MaxScrollBPM() + speed= math.round(poptions:MaxScrollBPM()) elseif poptions:TimeSpacing() > 0 then mode= "C" - speed= poptions:ScrollBPM() + speed= math.round(poptions:ScrollBPM()) else mode= "x" - speed= poptions:ScrollSpeed() + speed= math.round(poptions:ScrollSpeed() * 100) end ret.CurValues[pn]= {mode= mode, speed= speed} ret.NumPlayers= ret.NumPlayers + 1 diff --git a/Themes/_fallback/Scripts/03 ThemeAndGamePrefs.lua b/Themes/_fallback/Scripts/03 ThemeAndGamePrefs.lua index d2bf26e575..a91300ffbe 100644 --- a/Themes/_fallback/Scripts/03 ThemeAndGamePrefs.lua +++ b/Themes/_fallback/Scripts/03 ThemeAndGamePrefs.lua @@ -121,7 +121,6 @@ function OptionRowProTiming() setenv("ProTiming"..pname, val); --]] end; }; - setmetatable( t, t ); return t; end; @@ -202,6 +201,5 @@ function GamePrefDefaultFail() THEME:ReloadMetrics(); end; }; - setmetatable( t, t ); return t; end diff --git a/Themes/_fallback/metrics.ini b/Themes/_fallback/metrics.ini index c5e226edb1..d78f4b2c62 100644 --- a/Themes/_fallback/metrics.ini +++ b/Themes/_fallback/metrics.ini @@ -2941,11 +2941,13 @@ Line20="conf,VisualDelaySeconds" Fallback="ScreenOptionsServiceChild" NextScreen="ScreenOptionsService" PrevScreen="ScreenOptionsService" -LineNames="3,4,8,11,13,14,15,16,28,29,30,31" +LineNames="3,4,8,SI,SM,11,13,14,15,16,28,29,30,31" #LineScore="lua,UserPrefScoringMode()" Line3="conf,TimingWindowScale" Line4="conf,LifeDifficulty" Line8="lua,GamePrefDefaultFail()" +LineSI="lua,SpeedModIncSize()" +LineSM="lua,SpeedModIncMultiple()" Line11="conf,AllowW1" Line13="conf,HiddenSongs" Line14="conf,EasterEggs" @@ -3076,7 +3078,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,ArbSpeedMods()" +Line1="lua,ArbitrarySpeedMods()" # Line1="list,Speed" Line2="list,Accel" Line3A="list,EffectsReceptor" diff --git a/Themes/default/metrics.ini b/Themes/default/metrics.ini index 660b1dd4e8..eb3d5db5dc 100644 --- a/Themes/default/metrics.ini +++ b/Themes/default/metrics.ini @@ -1327,7 +1327,7 @@ LineFlashyCombo="lua,ThemePrefRow('FlashyCombo')" [ScreenOptionsGraphicsSound] [ScreenOptionsAdvanced] -LineNames="3,4,8,11,13,14,16,28,29,30,RollCombo" +LineNames="3,4,8,SI,SM,11,13,14,16,28,29,30,RollCombo" LineRollCombo="lua,ThemePrefRow('ComboOnRolls')" [ScreenAppearanceOptions] From d472d28490e0ed0f25ae8b1cdb09cbeb332ba9a4 Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Mon, 23 Jun 2014 00:25:18 -0600 Subject: [PATCH 4/5] Fixed accidental old name for ArbitrarySpeedMods in metrics for edit mode. --- Themes/default/metrics.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Themes/default/metrics.ini b/Themes/default/metrics.ini index eb3d5db5dc..3759e277ea 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,ArbSpeedMods()" +Line1="lua,ArbitrarySpeedMods()" LineSF="lua,OptionRowScreenFilter()" [StepsDisplayEdit] From dcf819d2977644b92388fd7e51a4a3e630900717 Mon Sep 17 00:00:00 2001 From: Kyzentun Date: Wed, 25 Jun 2014 12:08:57 -0600 Subject: [PATCH 5/5] Moved all sanity checking for OptionRowHandlerLua to a separate function so that a malformed row does not crash StepMania, and instead prints an error to the log file, and creates a row that does nothing. Added AllowAnything arg to CheckEnum. --- .../Examples/OptionRowHandlerLua.lua | 2 + Themes/_fallback/Languages/en.ini | 2 + src/EnumHelper.cpp | 15 +- src/EnumHelper.h | 8 +- src/LuaManager.h | 20 ++ src/OptionRowHandler.cpp | 273 ++++++++++++------ 6 files changed, 224 insertions(+), 96 deletions(-) diff --git a/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua index d4f8afc974..4072233bdc 100644 --- a/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua +++ b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua @@ -46,6 +46,8 @@ function FooMods() -- PlayerNumbers that are allowed to use the row. -- A row that can't be used by one player can be confusing for the players -- so consider carefully before using this. + -- This function will be called an extra time during loading to ensure + -- it returns a table. EnabledForPlayers= function(self) Trace("FooMods:EnabledForPlayers() called.") -- Leave out PLAYER_1 just for example. diff --git a/Themes/_fallback/Languages/en.ini b/Themes/_fallback/Languages/en.ini index 6ca37d22e9..237922b3cd 100644 --- a/Themes/_fallback/Languages/en.ini +++ b/Themes/_fallback/Languages/en.ini @@ -401,6 +401,7 @@ Hide=Hide Holds=Holds Insert=Insert Insert Credit=Insert Credit +Invalid=Invalid row definition. Key Joy Mappings=Map keys and joystick buttons to game functions. Language=Choose your language. LifeDifficulty=Increase this value to cause your life meter to drain faster and refill slower. @@ -975,6 +976,7 @@ Insert=Insert Insert Credit=Insert Credit Insert beat and shift down=Insert beat and shift down Insert Entry=Insert Entry +Invalid=Invalid Invert notes' player=Invert player notes (routine only) Jumps=Jumps Key Joy Mappings=Config Key/Joy Mappings diff --git a/src/EnumHelper.cpp b/src/EnumHelper.cpp index a7e7ec5b27..cc9d274f82 100644 --- a/src/EnumHelper.cpp +++ b/src/EnumHelper.cpp @@ -2,8 +2,9 @@ #include "EnumHelper.h" #include "LuaManager.h" #include "RageUtil.h" +#include "RageLog.h" -int CheckEnum( lua_State *L, LuaReference &table, int iPos, int iInvalid, const char *szType, bool bAllowInvalid ) +int CheckEnum( lua_State *L, LuaReference &table, int iPos, int iInvalid, const char *szType, bool bAllowInvalid, bool bAllowAnything ) { luaL_checkany( L, iPos ); @@ -61,6 +62,18 @@ int CheckEnum( lua_State *L, LuaReference &table, int iPos, int iInvalid, const LuaHelpers::Pop( L, sGot ); } LuaHelpers::Push( L, ssprintf("Expected %s; got %s", szType, sGot.c_str() ) ); + // There are a couple places where CheckEnum is used outside of a + // function called from lua. If we use lua_error from one of them, + // StepMania crashes out completely. bAllowAnything allows those places + // to avoid crashing over theme mistakes. + if(bAllowAnything) + { + RString errmsg; + LuaHelpers::Pop(L, errmsg); + LOG->Warn(errmsg.c_str()); + lua_pop(L, 2); + return iInvalid; + } lua_error( L ); } int iRet = lua_tointeger( L, -1 ); diff --git a/src/EnumHelper.h b/src/EnumHelper.h index 72d57381fc..c5209dc7a3 100644 --- a/src/EnumHelper.h +++ b/src/EnumHelper.h @@ -20,7 +20,8 @@ int CheckEnum(lua_State *L, int iPos, int iInvalid, const char *szType, - bool bAllowInvalid); + bool bAllowInvalid, + bool bAllowAnything= false); template struct EnumTraits @@ -36,14 +37,15 @@ template LuaReference EnumTraits::EnumToString; namespace Enum { template - static T Check( lua_State *L, int iPos, bool bAllowInvalid = false ) + static T Check( lua_State *L, int iPos, bool bAllowInvalid = false, bool bAllowAnything= false ) { return (T) CheckEnum(L, EnumTraits::StringToEnum, iPos, EnumTraits::Invalid, EnumTraits::szName, - bAllowInvalid); + bAllowInvalid, + bAllowAnything); } template static void Push( lua_State *L, T iVal ) diff --git a/src/LuaManager.h b/src/LuaManager.h index ea493ff97e..1dce0a906b 100644 --- a/src/LuaManager.h +++ b/src/LuaManager.h @@ -213,6 +213,26 @@ inline bool MyLua_checkintboolean( lua_State *L, int iArg ) return MyLua_checkboolean( L, iArg ); } +// Checks the table at index to verify that it contains strings. +inline bool TableContainsOnlyStrings(lua_State* L, int index) +{ + bool passed= true; + lua_pushnil(L); + while(lua_next(L, index) != 0) + { + // `key' is at index -2 and `value' at index -1 + const char *pValue = lua_tostring(L, -1); + if(pValue == NULL) + { + // Was going to print an error to the log with the key that failed, + // but didn't want to pull in RageLog. -Kyz + passed= false; + } + lua_pop(L, 1); + } + return passed; +} + #define SArg(n) (luaL_checkstring(L,(n))) #define BIArg(n) (MyLua_checkintboolean(L,(n))) #define IArg(n) (luaL_checkint(L,(n))) diff --git a/src/OptionRowHandler.cpp b/src/OptionRowHandler.cpp index 646927c212..dcab098562 100644 --- a/src/OptionRowHandler.cpp +++ b/src/OptionRowHandler.cpp @@ -809,7 +809,10 @@ public: LuaReference *m_pLuaTable; LuaReference m_EnabledForPlayersFunc; - OptionRowHandlerLua() { m_pLuaTable = new LuaReference; Init(); } + bool m_TableIsSane; + + OptionRowHandlerLua(): m_TableIsSane(false) + { m_pLuaTable = new LuaReference; Init(); } virtual ~OptionRowHandlerLua() { delete m_pLuaTable; } void Init() { @@ -817,8 +820,133 @@ public: m_pLuaTable->Unset(); } + bool SanityCheckTable(lua_State* L, RString& RowName) + { + if(m_pLuaTable->GetLuaType() != LUA_TTABLE) + { + LOG->Warn("LUA_ERROR: Result of \"%s\" is not a table.", RowName.c_str()); + return false; + } + m_pLuaTable->PushSelf(L); + lua_getfield(L, -1, "Name"); + const char *pStr = lua_tostring(L, -1); + if( pStr == NULL ) + { + LOG->Warn("LUA_ERROR: \"%s\" \"Name\" entry is not a string.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "LayoutType"); + pStr = lua_tostring(L, -1); + if(pStr == NULL || StringToLayoutType(pStr) == LayoutType_Invalid) + { + LOG->Warn("LUA_ERROR: \"%s\" \"LayoutType\" entry is not a string.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "SelectType"); + pStr = lua_tostring(L, -1); + if(pStr == NULL || StringToSelectType(pStr) == SelectType_Invalid) + { + LOG->Warn("LUA_ERROR: \"%s\" \"SelectType\" entry is not a string.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "Choices"); + if(!lua_istable(L, -1)) + { + LOG->Warn("LUA_ERROR: \"%s\" \"Choices\" is not a table.", RowName.c_str()); + return false; + } + if(!TableContainsOnlyStrings(L, lua_gettop(L))) + { + LOG->Warn("LUA_ERROR: \"%s\" \"Choices\" table contains a non-string.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "EnabledForPlayers"); + if(!lua_isnil(L, -1)) + { + if(!lua_isfunction(L, -1)) + { + LOG->Warn("LUA_ERROR: \"%s\" \"EnabledForPlayers\" is not a function.", RowName.c_str()); + return false; + } + m_pLuaTable->PushSelf( L ); + lua_call( L, 1, 1 ); // call function with 1 argument and 1 result + if(!lua_istable(L, -1)) + { + LOG->Warn("LUA_ERROR: \"%s\" \"EnabledForPlayers\" did not return a table.", RowName.c_str()); + return false; + } + lua_pushnil(L); + while(lua_next(L, -2) != 0) + { + PlayerNumber pn= Enum::Check(L, -1, true, true); + if(pn == PlayerNumber_Invalid) + { + LOG->Warn("LUA_ERROR: \"%s\" \"EnabledForPlayers\" contains a non-PlayerNumber.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + } + } + lua_pop(L, 1); + + lua_getfield(L, -1, "ReloadRowMessages"); + if(!lua_isnil(L, -1)) + { + if(!lua_istable(L, -1)) + { + LOG->Warn("LUA_ERROR: \"%s\" \"ReloadRowMessages\" is not a table.", RowName.c_str()); + return false; + } + if(!TableContainsOnlyStrings(L, lua_gettop(L))) + { + LOG->Warn("LUA_ERROR: \"%s\" \"ReloadRowMessages\" table contains a non-string.", RowName.c_str()); + return false; + } + } + lua_pop(L, 1); + + lua_getfield(L, -1, "LoadSelections"); + if(!lua_isfunction(L, -1)) + { + LOG->Warn("LUA_ERROR: \"%s\" \"LoadSelections\" entry is not a function.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "SaveSelections"); + if(!lua_isfunction(L, -1)) + { + LOG->Warn("LUA_ERROR: \"%s\" \"SaveSelections\" entry is not a function.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "NotifyOfSelection"); + if(!lua_isnil(L, -1) && !lua_isfunction(L, -1)) + { + LOG->Warn("LUA_ERROR: \"%s\" \"NotifyOfSelection\" entry is not a function.", RowName.c_str()); + return false; + } + lua_pop(L, 1); + + lua_pop(L, 1); + return true; + } + void SetEnabledForPlayers() { + if(!m_TableIsSane) + { + return; + } Lua *L = LUA->Get(); if( m_EnabledForPlayersFunc.IsNil() ) @@ -833,11 +961,6 @@ public: m_pLuaTable->PushSelf( L ); lua_call( L, 1, 1 ); // call function with 1 argument and 1 result - if( !lua_istable(L, -1) ) - { - LOG->Warn("LUA_ERROR: \"EnabledForPlayers\" did not return a table." ); - } - m_Def.m_vEnabledForPlayers.clear(); // and fill in with supplied PlayerNumbers below lua_pushnil( L ); @@ -869,113 +992,89 @@ public: // Run the Lua expression. It should return a table. m_pLuaTable->SetFromExpression( sLuaFunction ); - - if( m_pLuaTable->GetLuaType() != LUA_TTABLE ) + m_TableIsSane= SanityCheckTable(L, sLuaFunction); + if(!m_TableIsSane) { - LOG->Warn("LUA_ERROR: Result of \"%s\" is not a table.", sLuaFunction.c_str()); + m_pLuaTable->PushSelf(L); + lua_getfield(L, -1, "Name"); + const char *pStr = lua_tostring( L, -1 ); + if(pStr == NULL) + { + m_Def.m_sName = "Invalid"; + } + else + { + m_Def.m_sName = pStr; + } + lua_pop( L, 1 ); + // Add a fake choice so that there won't be a crash. + // This is so that a themer that makes a mistake doesn't have to + // completely restart and can just reload scripts. + m_Def.m_vsChoices.push_back("Error in row."); + // Set m_selectType to SELECT_MULTIPLE so we won't hit the assert in + // VerifySelected. + m_Def.m_selectType= SELECT_MULTIPLE; + lua_settop(L, 0); // Release has an assert that forces a clear stack. + LUA->Release(L); + return; } + m_pLuaTable->PushSelf(L); - m_pLuaTable->PushSelf( L ); - - lua_pushstring( L, "Name" ); - lua_gettable( L, -2 ); + lua_getfield(L, -1, "Name"); const char *pStr = lua_tostring( L, -1 ); - if( pStr == NULL ) - { - 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 ); + lua_getfield(L, -1, "OneChoiceForAllPlayers"); m_Def.m_bOneChoiceForAllPlayers = lua_toboolean( L, -1 ); lua_pop( L, 1 ); - lua_pushstring( L, "ExportOnChange" ); - lua_gettable( L, -2 ); + lua_getfield(L, -1, "ExportOnChange"); m_Def.m_bExportOnChange = lua_toboolean( L, -1 ); lua_pop( L, 1 ); - lua_pushstring( L, "LayoutType" ); - lua_gettable( L, -2 ); + // TODO: Change these to use the proper enum strings like everything + // else. This will break theme compatibility, so it has to wait until + // after SM5. -Kyz + lua_getfield(L, -1, "LayoutType"); pStr = lua_tostring( L, -1 ); - if( pStr == NULL ) - { - 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 ); - lua_pushstring( L, "SelectType" ); - lua_gettable( L, -2 ); + lua_getfield(L, -1, "SelectType"); pStr = lua_tostring( L, -1 ); - if( pStr == NULL ) - { - 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 ); // Iterate over the "Choices" table. - lua_pushstring( L, "Choices" ); - lua_gettable( L, -2 ); - if( !lua_istable( L, -1 ) ) - { - LOG->Warn("LUA_ERROR: \"%s\" \"Choices\" is not a table.", sLuaFunction.c_str()); - } - + lua_getfield(L, -1, "Choices"); 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("LUA_ERROR: \"%s\" Column entry is not a string.", sLuaFunction.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_pop( L, 1 ); // pop choices table // Set the EnabledForPlayers function. - lua_pushstring( L, "EnabledForPlayers" ); - lua_gettable( L, -2 ); - if( !lua_isfunction( L, -1 ) && !lua_isnil( L, -1 ) ) - { - LOG->Warn("LUA_ERROR: \"%s\" \"EnabledForPlayers\" is not a function.", sLuaFunction.c_str()); - } + lua_getfield(L, -1, "EnabledForPlayers"); m_EnabledForPlayersFunc.SetFromStack( L ); SetEnabledForPlayers(); // Iterate over the "ReloadRowMessages" table. - lua_pushstring( L, "ReloadRowMessages" ); - lua_gettable( L, -2 ); + lua_getfield(L, -1, "ReloadRowMessages"); if( !lua_isnil( L, -1 ) ) { - if( !lua_istable( L, -1 ) ) - RageException::Throw( "\"%s\" \"ReloadRowMessages\" is not a table.", sLuaFunction.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("LUA_ERROR: \"%s\" Column entry is not a string.", sLuaFunction.c_str()); - } //LOG->Trace( "Found ReloadRowMessage '%s'", pValue); - m_vsReloadRowMessages.push_back( pValue ); - lua_pop( L, 1 ); // removes `value'; keeps `key' for next iteration } } @@ -995,6 +1094,10 @@ public: virtual void ImportOption( OptionRow *pRow, const vector &vpns, vector vbSelectedOut[NUM_PLAYERS] ) const { + if(!m_TableIsSane) + { + return; + } Lua *L = LUA->Get(); ASSERT( lua_gettop(L) == 0 ); @@ -1019,12 +1122,7 @@ public: m_pLuaTable->PushSelf( L ); ASSERT( lua_istable( L, -1 ) ); - lua_pushstring( L, "LoadSelections" ); - lua_gettable( L, -2 ); - if( !lua_isfunction( L, -1 ) ) - { - LOG->Warn("LUA_ERROR: \"%s\" \"LoadSelections\" entry is not a function.", m_Def.m_sName.c_str()); - } + lua_getfield(L, -1, "LoadSelections"); // Argument 1 (self): m_pLuaTable->PushSelf( L ); @@ -1053,6 +1151,10 @@ public: } virtual int ExportOption( const vector &vpns, const vector vbSelected[NUM_PLAYERS] ) const { + if(!m_TableIsSane) + { + return 0; + } Lua *L = LUA->Get(); ASSERT( lua_gettop(L) == 0 ); @@ -1075,12 +1177,7 @@ public: m_pLuaTable->PushSelf( L ); ASSERT( lua_istable( L, -1 ) ); - lua_pushstring( L, "SaveSelections" ); - lua_gettable( L, -2 ); - if( !lua_isfunction( L, -1 ) ) - { - LOG->Warn("LUA_ERROR: \"%s\" \"SaveSelections\" entry is not a function.", m_Def.m_sName.c_str()); - } + lua_getfield(L, -1, "SaveSelections"); // Argument 1 (self): m_pLuaTable->PushSelf( L ); @@ -1109,11 +1206,14 @@ public: } virtual bool NotifyOfSelection(PlayerNumber pn, int choice) { + if(!m_TableIsSane) + { + return false; + } Lua *L= LUA->Get(); m_pLuaTable->PushSelf(L); - lua_pushstring(L, "NotifyOfSelection"); - lua_gettable(L, -2); + lua_getfield(L, -1, "NotifyOfSelection"); bool changed= false; if(lua_isfunction(L, -1)) { @@ -1128,25 +1228,14 @@ public: 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_getfield(L, -1, "Choices"); 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 } }