Handling custom PlayerOptions - RhythmLunatic/stepmania GitHub Wiki
This tutorial has not been tested. If it doesn't work create an issue on the repo.
In order to add more options to a profile, you must implement custom saving/handling of variables. Since I only use SL-PlayerOptions, this tutorial will be how to implement RIO's hacked up version of SL-PlayerOptions.
SL-PlayerOptions is short for "Simply Love-PlayerOptions", as the system was originally written by quietly-turning for Simply Love.
1. Add the script
Put this script in your Scripts folder. You should remove the profile icon handling if you don't need it, otherwise you could use it for something like a PIU Infinity theme. Do not rename the functions, they're set in metrics.ini by default under the [Profile]
section and called by StepMania during profile load/save. There is no need to manually call SaveProfileCustom or LoadProfileCustom at any point... Normally. Scroll down to #5 for more information.
https://github.com/RhythmLunatic/raveitout/blob/master/Scripts/04%20SL-CustomProfiles.lua
2. Define default options
RIO is a modified version of SL-PlayerOptions. Instead of env vars there is a global table where all player options are kept. 03 PlayerOptions.lua details how the table works, but it will also be repeated here.
Define a global table named PlayerDefaults. Here is an example.
--[[
This defines the custom player options. PlayerDefaults is initialized from InitGame.lua
Use ActiveModifiers["P1"] or ActiveModifiers["P2"] to access options. ActiveModifiers
is automatically set when the profile is loaded.
]]
PlayerDefaults = {
DetailedPrecision = false, --Options: false, EarlyLate, ProTiming
ScreenFilter = 0,
BGAMode = "On", --Options: Black, Off, Dark, On
JudgmentGraphic = "Season2", --Judgment graphi
}
3. Init PlayerOptions
In a script anywhere, insert this helper function. For RIO it is in 03 PlayerOptions.lua. Make sure the helper function is loaded first, of course.
function table.shallowcopy(orig)
local orig_type = type(orig)
local copy
if orig_type == 'table' then
copy = {}
for orig_key, orig_value in pairs(orig) do
copy[orig_key] = orig_value
end
else -- number, string, boolean, etc
copy = orig
end
return copy
end
Now use the helper function to copy the default values to the player values. This ensures that a brand new profile without any previously set options gets the default values, along with any new values added. Any changed values will be overwritten by SL-PlayerOptions' LoadProfileCustom function.
You must copy the default to the the player values at the beginning of each round before the login screen so any changed values brought over from the previous round are erased. (Those of you who know what you're doing: Yes, you can use setenv instead to ensure the table is deleted at the end of the round.)
For RIO this process is done in 99 InitGame.lua and 99 InitGame.lua is called in the screen where our team logo is shown (After the game over screen, during the attract loop, after exiting the system options).
--It may be advantageous for you to do ["PlayerNumber_P1"] = table.shallowcopy(PlayerDefaults) instead if you would like to access the player options using ActiveModifiers[PLAYER_1] instead of ActiveModifiers["P1"].
ActiveModifiers = {
P1 = table.shallowcopy(PlayerDefaults),
P2 = table.shallowcopy(PlayerDefaults),
}
4. Reading/Writing player options (As a ScreenPlayerOptions or OptionsList child)
This is mostly outside the scope of this tutorial but here is an example of how to set/read the option from the Options Screen/Menu.
In this example you have something like this in metrics.ini...
[ScreenOptionsMaster]
System="8"
SystemDefault=""
...
System,5="name,DetailedPrecision;screen,DetailedPrecision"
...
[OptionsList]
LineCompetitionMode="lua,OptionRowCompetitionMode()"
And the option itself is:
function OptionRowCompetitionMode()
local t = {
Name = "CompetitionMode";
LayoutType = "ShowAllInRow";
SelectType = "SelectOne";
OneChoiceForAllPlayers = false;
ExportOnChange = false;
Choices = { "Off", "On"}; --The names of the choices shown to the player.
LoadSelections = function(self, list, pn)
local compMode = ActiveModifiers[pname(pn)]['CompetitionMode'] --Get the player's competition mode preference
if compMode == true then --You don't really need the == true here but whatever
list[2] = true; --Make the "On" choice selected
else
list[1] = true; --Else, make the "Off" choice selected
end;
end;
SaveSelections = function(self, list, pn)
--If list[2] (The On choice) is selected, then it would get set true in ActiveModifiers. If it's not selected, it's false, so it gets set false in ActiveModifiers.
ActiveModifiers[pname(pn)]['CompetitionMode'] = list[2];
end;
};
setmetatable( t, t );
return t;
end;
4b. Reading the value outside of an Option
For using theme elements and such. It is fairly simple. Here is an example taken from Player judgment.lua where player is either PLAYER_1 or PLAYER_2.
local judgmentToLoad = ActiveModifiers[pname(player)]["JudgmentGraphic"];
5. Saving at the end of the round (Optional?)
It seems that while StepMania is supposed to know when to save a profile there are times when it does not occur, perhaps due to using a custom stage system like RIO or maybe it's a StepMania bug. Either way, it can be manually called. Only do this if profile saving is not functioning properly, as saving twice wastes time and file access is slow!
- If your theme uses ScreenProfileSaveSummary it can be done at that point, as ScreenProfileSaveSummary is used instead of ScreenProfileSave when the round is over.
- If you're using Event Mode you should be saving after every song during ScreenProfileSave.
Find and modify LoadCommand of ScreenProfileSave overlay...
local isGameOver = Var("LoadingScreen") == "ScreenProfileSaveSummary"
t[#t+1] = Def.Actor {
BeginCommand=function(self)
self:queuecommand("Load");
end;
LoadCommand=function()
--Workaround for SaveProfileCustom not being called by SM
for player in ivalues(GAMESTATE:GetHumanPlayers()) do
--Will return "" if the player is using the machine profile.
local profileDir = PROFILEMAN:GetProfileDir(ProfileSlot[PlayerNumber:Reverse()[player]+1]);
--Don't save if it's the machine profile.
if profileDir ~= "" then
SaveProfileCustom(PROFILEMAN:GetProfile(player),profileDir);
end;
--If there are no stages left, save data needed for memory cards. StepMania is supposed to handle this but it's broken...
if isGameOver or GAMESTATE:IsEventMode() then
if PROFILEMAN:ProfileWasLoadedFromMemoryCard(player) then
PROFILEMAN:SaveProfile(player);
end;
end;
end;
SCREENMAN:GetTopScreen():Continue();
end;
};