diff --git a/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua new file mode 100644 index 0000000000..4072233bdc --- /dev/null +++ b/Docs/Themerdocs/Examples/OptionRowHandlerLua.lua @@ -0,0 +1,141 @@ +-- 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()" + +-- 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. + +-- 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 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. + -- 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. + 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 examine 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 + 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 + end + elseif change == 3 then + for i, choice in ipairs(self.Choices) do + 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 + } +end diff --git a/Themes/_fallback/Languages/en.ini b/Themes/_fallback/Languages/en.ini index b1ed7adaad..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. @@ -478,6 +479,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. @@ -973,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 @@ -1099,6 +1103,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 9ce066d813..43824914c3 100644 --- a/Themes/_fallback/Scripts/03 CustomSpeedMods.lua +++ b/Themes/_fallback/Scripts/03 CustomSpeedMods.lua @@ -243,10 +243,223 @@ function SpeedMods() state:SetPlayerOptions("ModsLevel_Preferred", self.Choices[1]) end } - setmetatable( t, t ) return t end +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", + 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 + 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) + 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 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 + val.mode= ({"x", "C", "m"})[real_choice - 4] + 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 + local big_inc= increment * multiple + local small_inc= increment + if show_x_incs then + big_inc= tostring(big_inc / 100) + small_inc= tostring(small_inc / 100) + else + 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/100) .. "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= math.round(poptions:MaxScrollBPM()) + elseif poptions:TimeSpacing() > 0 then + mode= "C" + speed= math.round(poptions:ScrollBPM()) + else + mode= "x" + speed= math.round(poptions:ScrollSpeed() * 100) + 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/Scripts/03 ThemeAndGamePrefs.lua b/Themes/_fallback/Scripts/03 ThemeAndGamePrefs.lua index fb26f0912a..3e8311486c 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; diff --git a/Themes/_fallback/metrics.ini b/Themes/_fallback/metrics.ini index ffd8d8d3e6..b58fc22369 100644 --- a/Themes/_fallback/metrics.ini +++ b/Themes/_fallback/metrics.ini @@ -2942,11 +2942,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" @@ -3077,7 +3079,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,ArbitrarySpeedMods()" # Line1="list,Speed" Line2="list,Accel" Line3A="list,EffectsReceptor" diff --git a/Themes/default/metrics.ini b/Themes/default/metrics.ini index 1435f2789c..3759e277ea 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] @@ -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,ArbitrarySpeedMods()" LineSF="lua,OptionRowScreenFilter()" [StepsDisplayEdit] 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/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..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,16 +961,13 @@ public: m_pLuaTable->PushSelf( L ); 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." ); - m_Def.m_vEnabledForPlayers.clear(); // and fill in with supplied PlayerNumbers below lua_pushnil( L ); 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 ); @@ -867,111 +992,94 @@ public: // Run the Lua expression. It should return a table. m_pLuaTable->SetFromExpression( sLuaFunction ); + m_TableIsSane= SanityCheckTable(L, sLuaFunction); + if(!m_TableIsSane) + { + 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); - if( m_pLuaTable->GetLuaType() != LUA_TTABLE ) - RageException::Throw( "Result of \"%s\" is not a table.", sLuaFunction.c_str() ); - - 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 ) - RageException::Throw( "\"%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 ); + 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 ); - m_Def.m_bExportOnChange = !!lua_toboolean( L, -1 ); + 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 ) - RageException::Throw( "\"%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 ) - RageException::Throw( "\"%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 ) ) - RageException::Throw( "\"%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 ) - RageException::Throw( "\"%s\" Column entry is not a string.", sLuaFunction.c_str() ); -// LOG->Trace( "'%s'", pValue); - + //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 ) ) - RageException::Throw( "\"%s\" \"EnabledForPlayers\" is not a table.", 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 ) - RageException::Throw( "\"%s\" Column entry is not a string.", sLuaFunction.c_str() ); - LOG->Trace( "Found ReloadRowMessage '%s'", pValue); - + //LOG->Trace( "Found ReloadRowMessage '%s'", pValue); m_vsReloadRowMessages.push_back( pValue ); - lua_pop( L, 1 ); // removes `value'; keeps `key' for next iteration } } 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 ); @@ -986,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 ); @@ -1010,10 +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 ) ) - RageException::Throw( "\"%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 ); @@ -1042,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 ); @@ -1064,10 +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 ) ) - RageException::Throw( "\"%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 ); @@ -1094,6 +1204,46 @@ public: // XXX: allow specifying the mask return 0; } + virtual bool NotifyOfSelection(PlayerNumber pn, int choice) + { + if(!m_TableIsSane) + { + return false; + } + Lua *L= LUA->Get(); + m_pLuaTable->PushSelf(L); + + lua_getfield(L, -1, "NotifyOfSelection"); + 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_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 ); + //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();