Files
Dan Guzek 9666bb0ae2 change a few global Lua variables to be local
Running StepMania with the command line argument of --ExportLuaInformation while the current 5.1's default theme (Lambda) was selected added some seemingly stray Lua variables to the Constants table in ./Docs/Luadoc/Lua.xml.

I spotted them here: http://dguzek.github.io/Lua-For-SM5/LuaAPI#Constants

I'm assuming num, bPreference, and bCategory don't need global scope, so I added the local keyword where appropriate.
2019-12-09 19:45:57 -05:00

488 lines
14 KiB
Lua

--[[
Custom Speed Mods v3 (for StepMania 5)
changelog:
v3 (StepMania 5 b3)
* Complete rewrite to use profile load/save hooks.
--------------------------------------------------------------------------------
v2.3 (StepMania 5 a2/SM5TE) [by AJ]
* If someone has decided to remove 1x from the machine profile's speed mods,
silently fix it.
* Ignore Cmod and mmod capitalization errors.
v2.2 (StepMania 5 alpha 2) [by FSX]
* Rewrite table management code.
* Add code to make sure that there are speed mods and that they are correct.
v2.1 (StepMania 5 Preview 2)
* Added support for m-Mods.
v2.0 (for sm-ssc)
Giant rewrite of the speed mod parser.
This rewrite comes with the following changes/features:
* Speed mods are now tied to profiles.
This is arguably the biggest change, as it allows the speed mods to be
portable, as well as per-profile.
Thanks to this, we can now support reading SpeedMods from a USB stick or
other external storage. (I didn't test writing yet, but it should work.)
This version of Custom Speed Mods will only run on StepMania 5 (due to m-mods).
--------------------------------------------------------------------------------
v1.4
* Try to auto-set the speed mod to 1.0 if:
1) The player hasn't already chosen a speed mod
2) The player's custom speed mod collection starts with a value under 1x.
Due to the way the custom speed mods were coded, it will always pick the
first value, even if it's not 1.0x.
v1.3
* strip whitespace out of file in case people use it.
(I don't think it really works but SM seems to think the mods are legal)
* fixed an error related to using the fallback return value.
v1.2
* small fixes
* more comments
v1.1
* Cleaned up code some, I think.
]]
local ProfileSpeedMods = {}
-- Returns a new, empty mod table: a table with three members x, C, and m,
-- each being a table with the corresponding numbers set to true.
local function EmptyModTable()
return {x = {}, C = {}, m = {}}
end
-- Merge one mod table into another.
local function MergeInModTable(dst, src)
for typ, subtbl in pairs(src) do
for n, v in pairs(subtbl) do
dst[typ][n] = v
end
end
end
-- Parses a speed mod and returns the pair (type, number) or nil if parsing
-- failed.
local function CanonicalizeMod(mod)
local num = tonumber(mod:match("^(%d+.?%d*)[xX]$"))
if num ~= nil then
return "x", num
end
num = tonumber(mod:match("^[cC](%d+.?%d*)$"))
if num ~= nil then
return "C", num
end
num = tonumber(mod:match("^[mM](%d+.?%d*)$"))
if num ~= nil then
return "m", num
end
return nil
end
-- Parse a comma-separated string into a mod table.
local function StringToModTable(str)
local mods = EmptyModTable()
local valid = false
string.gsub(str, "%s", "")
for _, mod in ipairs(split(",", str)) do
local t, n = CanonicalizeMod(mod)
if t then
mods[t][n] = true
valid = true
end
end
return valid and mods or nil
end
-- Return the contents of a mod table as a list of mod names.
local function ModTableToList(mods)
local l = {}
local tmp = {}
-- Do x-mods separately because the x comes after
for mod, _ in pairs(mods.x) do
table.insert(tmp, mod)
end
table.sort(tmp)
for _, mod in ipairs(tmp) do
table.insert(l, mod .. "x")
end
-- C- and m-mods
for _, modtype in ipairs({"C", "m"}) do
tmp = {}
for mod, _ in pairs(mods[modtype]) do
table.insert(tmp, mod)
end
table.sort(tmp)
for _, mod in ipairs(tmp) do
table.insert(l, modtype .. mod)
end
end
return l
end
local DefaultMods = StringToModTable("0.5x,1x,1.5x,2x,3x,4x,5x,6x,7x,8x,C250,C450,m550")
-- Reads the custom speed mod file at <path> and returns a corresponding mod
-- table.
local function ReadSpeedModFile(path)
local file = RageFileUtil.CreateRageFile()
if not file:Open(path, 1) then
file:destroy()
return nil
end
local contents = file:Read()
file:Close()
file:destroy()
return StringToModTable(contents)
end
-- Hook called during profile load
function LoadProfileCustom(profile, dir)
-- This will be (intentionally) nil if the file is missing or bad
local mods = ReadSpeedModFile(dir .. "SpeedMods.txt")
-- Special case for the machine profile
if profile == PROFILEMAN:GetMachineProfile() then
ProfileSpeedMods.machine = mods
return
end
-- Otherwise, it's a player profile. Store accordingly.
for i = 1, NUM_PLAYERS do
if profile == PROFILEMAN:GetProfile(PlayerNumber[i]) then
ProfileSpeedMods[PlayerNumber[i]] = mods
break
end
end
end
-- Hook called during profile save
function SaveProfileCustom(profile, dir)
-- Change this if a theme allows you to change and save custom
-- per-profile settings.
end
-- Returns a list of speed mods for the current round.
local function GetSpeedMods()
-- Start with machine profile
local mods = ProfileSpeedMods.machine or EmptyModTable()
-- Merge in any active players
for _, p in ipairs(GAMESTATE:GetHumanPlayers()) do
if ProfileSpeedMods[p] and PROFILEMAN:IsPersistentProfile(p) then
MergeInModTable(mods, ProfileSpeedMods[p])
else
MergeInModTable(mods, DefaultMods)
end
end
-- Apparently removing 1x caused crashes, so be sure it's there.
-- (This may not be a problem anymore. -- djpohly)
mods.x[1] = true
return ModTableToList(mods)
end
-- Implementation of custom Lua option row
function SpeedMods()
local t = {
Name = "Speed",
LayoutType = "ShowAllInRow",
SelectType = "SelectOne",
OneChoiceForAllPlayers = false,
ExportOnChange = false,
Choices = GetSpeedMods(),
LoadSelections = function(self, list, pn)
local pref = GAMESTATE:GetPlayerState(pn):GetPlayerOptionsString("ModsLevel_Preferred")
local selected = 0
for i, choice in ipairs(self.Choices) do
if string.find(pref, choice) then
-- Found it, use it
selected = i
break
elseif choice == "1x" then
-- Pick this unless we find the
-- preferred choice
selected = i
end
end
-- If we didn't find a match, just use the first
if selected ~= 0 then
list[selected] = true
else
list[1] = true
end
end,
SaveSelections = function(self, list, pn)
local state = GAMESTATE:GetPlayerState(pn)
for i, choice in ipairs(self.Choices) do
if list[i] then
state:SetPlayerOptions("ModsLevel_Preferred", choice)
return
end
end
-- Or use the first
state:SetPlayerOptions("ModsLevel_Preferred", self.Choices[1])
end
}
return t
end
local default_speed_increment= 25
local default_speed_inc_large= 100
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_inc_large()
local inc_large= default_speed_inc_large
if ReadGamePrefFromFile("SpeedIncLarge") then
inc_large= tonumber(GetGamePref("SpeedIncLarge")) or default_speed_inc_large
else
WriteGamePrefToFile("SpeedIncLarge", inc_large)
end
return inc_large
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 SpeedModIncLarge()
-- An option row for controlling the size of the increment used by
-- ArbitrarySpeedMods.
local inc_large= get_speed_inc_large()
local ret= {
Name= "Speed Increment Large",
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("SpeedIncLarge", inc_large)
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= inc_large + incs[choice-1]
if new_val > 0 then
inc_large= new_val
end
self:GenChoices()
return true
end,
GenChoices= function(self)
self.Choices= {tostring(inc_large), "+10", "+1", "-1", "-10"}
end
}
ret:GenChoices()
return ret
end
function GetSpeedModeAndValueFromPoptions(pn)
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
return speed, mode
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 inc_large= get_speed_inc_large()
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 incs= {inc_large, increment, -increment, -inc_large}
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()
MESSAGEMAN:Broadcast("SpeedChoiceChanged", {pn= pn, mode= val.mode, speed= val.speed})
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= inc_large
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
if GAMESTATE:IsHumanPlayer(pn) then
local speed, mode= GetSpeedModeAndValueFromPoptions(pn)
ret.CurValues[pn]= {mode= mode, speed= speed}
ret.NumPlayers= ret.NumPlayers + 1
end
end
ret:GenChoices()
return ret
end
--[[
CustomSpeedMods (c) 2013 StepMania team.
Use freely, so long as this notice and the above documentation remains.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Previous version was copyright © 2008-2012 AJ Kelly/KKI Labs.
]]