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.

This commit is contained in:
Kyzentun
2014-06-19 06:41:18 -06:00
parent 006fcd84a7
commit 045e9edcdd
9 changed files with 361 additions and 36 deletions
@@ -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
+121 -1
View File
@@ -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.
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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]
+30 -5
View File
@@ -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<PlayerNumber> vpns;
FOREACH_HumanPlayer( p )
vpns.push_back( p );
ImportOptions(vpns);
FOREACH_PlayerNumber(p)
{
PositionUnderlines(p);
}
UpdateEnabledDisabled();
}
return changed;
}
void OptionRow::SetExitText( RString sExitText )
+5 -2
View File
@@ -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();
+86 -25
View File
@@ -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<PlayerNumber>(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
+2
View File
@@ -178,6 +178,8 @@ public:
virtual int ExportOption( const vector<PlayerNumber> &, const vector<bool> 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. */
+5 -1
View File
@@ -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();