diff --git a/Docs/Changelog_sm5.txt b/Docs/Changelog_sm5.txt index 763e34f4a9..f733ebb2d8 100644 --- a/Docs/Changelog_sm5.txt +++ b/Docs/Changelog_sm5.txt @@ -8,6 +8,32 @@ ________________________________________________________________________________ StepMania 5.0 ???? ?? | 20120??? -------------------------------------------------------------------------------- +2012/07/06 +---------- +* [ScoreDisplayRave] Add FrameBase and FrameOver to the metrics. [AJ] + +2012/07/05 +---------- +* [ScreenRanking] Added "NextPage" command. "SwitchPage" command is + now "SwitchedPage". [AJ] +* [ScreenRanking] Don't clobber alpha values when applying colors in SetPage. [AJ] + +2012/07/03 +---------- +* [ScreenHowToPlay] Actually shows notes again. [AJ] +* [ScreenGameplay] When ShowLifeMeterForDisabledPlayers is true, don't fill up + life in the meter for the disabled player. (LifeMeterBar only) [AJ] + +2012/07/02 +---------- +* [ScreenHowToPlay] Run InitCommand on Characters if enabled. [AJ] +* [RageUtil] Add detection of ".hg" to StripCvsAndSvn. [AJ] + +2012/06/27 +---------- +* [SongManager] Ending a group's name in PreferredSongs with "/*" will now load + all songs from that group automatically. [AJ] + 2012/06/21 ---------- * [ProfileManager] Added GetPlayerName(pn) Lua binding. [AJ] diff --git a/Themes/_fallback/Languages/pl.ini b/Themes/_fallback/Languages/pl.ini index 1ca787f532..de8a5ecee7 100755 --- a/Themes/_fallback/Languages/pl.ini +++ b/Themes/_fallback/Languages/pl.ini @@ -1732,6 +1732,8 @@ Edit/Share=Edycja Exit=Wyjście Game Start=Start Jukebox=Odtwarzacz +Report Bug=Zgłoś błąd! +Chat on IRC=Dyskutuj na IRC! Select Game=Wybór trybu gry Theme=Motyw graficzny Credits=Autorzy diff --git a/Themes/_fallback/Other/ScreenGameplaySyncMachine music.ssc b/Themes/_fallback/Other/ScreenGameplaySyncMachine music.ssc index 5721dff9bf..3e20c4612e 100644 --- a/Themes/_fallback/Other/ScreenGameplaySyncMachine music.ssc +++ b/Themes/_fallback/Other/ScreenGameplaySyncMachine music.ssc @@ -1,173 +1,173 @@ -#TITLE:Sync; -#SUBTITLE:; -#ARTIST:Unknown artist; -#TITLETRANSLIT:; -#SUBTITLETRANSLIT:; -#ARTISTTRANSLIT:; -#GENRE:; -#CREDIT:; -#BANNER:; -#BACKGROUND:; -#LYRICSPATH:; -#CDTITLE:; -#MUSIC:_sync music.ogg; -#OFFSET:-0.012; -#SAMPLESTART:0.000; -#SAMPLELENGTH:12.000; -#SELECTABLE:YES; -#BPMS:0.000=132.000; -#STOPS:; -#BGCHANGES:; -#KEYSOUNDS:; - -#NOTEDATA:; -#STEPSTYPE:dance-single:; -#DESCRIPTION:blank; -#DIFFICULTY:Beginner; -#METER:1; -#RADARVALUES:0.302,0.205,0.000,0.000,0.000; -#BPMS:0.000=132.000; -#OFFSET:-0.012; -#NOTES: - // measure 1 -0000 -, // measure 2 -1000 -0001 -1000 -0001 -1000 -0001 -1000 -0001 -, // measure 3 -1000 -0001 -1000 -0001 -1000 -0001 -1000 -0001 -, // measure 4 -1000 -0001 -1000 -0001 -1000 -0001 -1000 -0001 -, // measure 5 -1000 -0001 -1000 -0001 -1000 -0001 -1000 -0001 -, // measure 6 -1000 -0001 -1000 -0001 -1000 -0001 -1000 -0001 -, // measure 7 -1000 -0001 -1000 -0001 -1000 -0001 -1000 -0001 -, // measure 8 -1000 -0001 -1000 -0001 -1000 -0001 -1000 -0001 -; - -#NOTEDATA:; -#STEPSTYPE:pump-single; -#DESCRIPTION:blank; -#DIFFICULTY:Beginner; -#METER:1; -#RADARVALUES:0.302,0.205,0.000,0.000,0.000; -#BPMS:0.000=132.000; -#OFFSET:-0.012; -#NOTES: - // measure 1 -00000 -, // measure 2 -10000 -00001 -10000 -00001 -10000 -00001 -10000 -00001 -, // measure 3 -10000 -00001 -10000 -00001 -10000 -00001 -10000 -00001 -, // measure 4 -10000 -00001 -10000 -00001 -10000 -00001 -10000 -00001 -, // measure 5 -10000 -00001 -10000 -00001 -10000 -00001 -10000 -00001 -, // measure 6 -10000 -00001 -10000 -00001 -10000 -00001 -10000 -00001 -, // measure 7 -10000 -00001 -10000 -00001 -10000 -00001 -10000 -00001 -, // measure 8 -10000 -00001 -10000 -00001 -10000 -00001 -10000 -00001 -; +#TITLE:Sync; +#SUBTITLE:; +#ARTIST:Unknown artist; +#TITLETRANSLIT:; +#SUBTITLETRANSLIT:; +#ARTISTTRANSLIT:; +#GENRE:; +#CREDIT:; +#BANNER:; +#BACKGROUND:; +#LYRICSPATH:; +#CDTITLE:; +#MUSIC:_sync music.ogg; +#OFFSET:-0.025; +#SAMPLESTART:0.000; +#SAMPLELENGTH:12.000; +#SELECTABLE:YES; +#BPMS:0.000=132.027; +#STOPS:; +#BGCHANGES:; +#KEYSOUNDS:; + +#NOTEDATA:; +#STEPSTYPE:dance-single; +#DESCRIPTION:blank; +#DIFFICULTY:Beginner; +#METER:1; +#RADARVALUES:0.302,0.205,0.000,0.000,0.000; +#BPMS:0.000=132.027; +#OFFSET:-0.025; +#NOTES: + // measure 1 +0000 +, // measure 2 +1000 +0001 +1000 +0001 +1000 +0001 +1000 +0001 +, // measure 3 +1000 +0001 +1000 +0001 +1000 +0001 +1000 +0001 +, // measure 4 +1000 +0001 +1000 +0001 +1000 +0001 +1000 +0001 +, // measure 5 +1000 +0001 +1000 +0001 +1000 +0001 +1000 +0001 +, // measure 6 +1000 +0001 +1000 +0001 +1000 +0001 +1000 +0001 +, // measure 7 +1000 +0001 +1000 +0001 +1000 +0001 +1000 +0001 +, // measure 8 +1000 +0001 +1000 +0001 +1000 +0001 +1000 +0001 +; + +#NOTEDATA:; +#STEPSTYPE:pump-single; +#DESCRIPTION:blank; +#DIFFICULTY:Beginner; +#METER:1; +#RADARVALUES:0.302,0.205,0.000,0.000,0.000; +#BPMS:0.000=132.027; +#OFFSET:-0.025; +#NOTES: + // measure 1 +00000 +, // measure 2 +10000 +00001 +10000 +00001 +10000 +00001 +10000 +00001 +, // measure 3 +10000 +00001 +10000 +00001 +10000 +00001 +10000 +00001 +, // measure 4 +10000 +00001 +10000 +00001 +10000 +00001 +10000 +00001 +, // measure 5 +10000 +00001 +10000 +00001 +10000 +00001 +10000 +00001 +, // measure 6 +10000 +00001 +10000 +00001 +10000 +00001 +10000 +00001 +, // measure 7 +10000 +00001 +10000 +00001 +10000 +00001 +10000 +00001 +, // measure 8 +10000 +00001 +10000 +00001 +10000 +00001 +10000 +00001 +; diff --git a/Themes/_fallback/Scripts/03 Gameplay.lua b/Themes/_fallback/Scripts/03 Gameplay.lua index 60168e0eb2..fb8a94cd6a 100644 --- a/Themes/_fallback/Scripts/03 Gameplay.lua +++ b/Themes/_fallback/Scripts/03 Gameplay.lua @@ -1,349 +1,350 @@ --- sm-ssc fallback theme | script ring 03 | Gameplay.lua --- [en] This file is used to store settings that should be different in each --- game mode. - --- shakesoda calls this pump.lua -local function CurGameName() - return GAMESTATE:GetCurrentGame():GetName() -end - --- Check the active game mode against a string. Cut down typing this in metrics. -function IsGame(str) return CurGameName() == str end - --- GetExtraColorThreshold() --- [en] returns the difficulty threshold in meter --- for songs that should be counted as boss songs. -function GetExtraColorThreshold() - local Modes = { - dance = 10, - pump = 21, - beat = 12, - kb7 = 10, - para = 10, - techno = 10, - lights = 10, -- lights shouldn't be playable - } - return Modes[CurGameName()] -end - --- AllowOptionsMenu() --- [en] returns if you are able to select options --- on ScreenSelectMusic. -function AllowOptionsMenu() - if GAMESTATE:IsAnExtraStage() then - return false - elseif GAMESTATE:GetPlayMode() == "PlayMode_Oni" then - return false - else - return true - end -end - --- GameCompatibleModes: --- [en] returns possible modes for ScreenSelectPlayMode -function GameCompatibleModes() - local Modes = { - dance = "Single,Double,Solo,Versus,Couple", - pump = "Single,Double,HalfDouble,Versus,Couple,Routine", - beat = "5Keys,7Keys,10Keys,14Keys,Versus5,Versus7", - kb7 = "KB7", - para = "Single", - -- todo: add versus modes for technomotion - techno = "Single4,Single5,Single8,Double4,Double5,Double8", - lights = "Single" -- lights shouldn't be playable - } - return Modes[CurGameName()] -end - -function SelectProfileKeys() - local sGame = CurGameName() - if sGame == "pump" then - return "Up,Down,Start,Back,Center,DownLeft,DownRight" - elseif sGame == "dance" then - return "Up,Down,Start,Back,Up2,Down2" - else - return "Up,Down,Start,Back" - end -end - --- ScoreKeeperClass: --- [en] Determines the correct ScoreKeeper class to use. -function ScoreKeeperClass() - -- rave scorekeeper - if GAMESTATE:GetPlayMode() == 'PlayMode_Rave' then return "ScoreKeeperRave" end - if GAMESTATE:GetCurrentStyle() then - local styleType = GAMESTATE:GetCurrentStyle():GetStyleType() - if styleType == 'StyleType_TwoPlayersSharedSides' then return "ScoreKeeperShared" end - end - return "ScoreKeeperNormal" -end - --- ComboContinue: --- [en] -function ComboContinue() - local Continue = { - dance = GAMESTATE:GetPlayMode() == "PlayMode_Oni" and "TapNoteScore_W2" or "TapNoteScore_W3", - pump = "TapNoteScore_W3", - beat = "TapNoteScore_W3", - kb7 = "TapNoteScore_W3", - para = "TapNoteScore_W4" - } - return Continue[CurGameName()] -end - -function ComboMaintain() - local Maintain = { - dance = "TapNoteScore_W3", - pump = "TapNoteScore_W4", - beat = "TapNoteScore_W3", - kb7 = "TapNoteScore_W3", - para = "TapNoteScore_W4" - } - return Maintain[CurGameName()] -end - -function ComboPerRow() - sGame = CurGameName() - if sGame == "pump" then - return true - elseif GAMESTATE:GetPlayMode() == "PlayMode_Oni" then - return true - else - return false - end -end - -function EvalUsesCheckpointsWithJudgments() - return (CurGameName() == "pump") and true or false -end - -local ComboThresholds = { - dance = { Hit = 2, Miss = 2, Fail = -1 }, - pump = { Hit = 4, Miss = 4, Fail = 51 }, - beat = { Hit = 1, Miss = 0, Fail = -1 }, - kb7 = { Hit = 1, Miss = 0, Fail = -1 }, - para = { Hit = 2, Miss = 0, Fail = -1 }, - ------------------------------------------- - default = { Hit = 2, Miss = 2, Fail = -1 } -} - -function HitCombo() - if ComboThresholds[CurGameName()] then - return ComboThresholds[CurGameName()].Hit - end - return ComboThresholds["default"].Hit -end - -function MissCombo() - if ComboThresholds[CurGameName()] then - return ComboThresholds[CurGameName()].Miss - end - return ComboThresholds["default"].Miss -end - --- FailCombo: --- [en] The combo that causes game failure. -function FailCombo() - -- ITG (dance) uses 30. Pump Pro uses 30, real Pump uses 51 - if ComboThresholds[CurGameName()] then - return ComboThresholds[CurGameName()].Fail - end - return ComboThresholds["default"].Fail -end - -local RoutineSkins = { - dance = { P1 = "midi-routine-p1", P2 = "midi-routine-p2" }, - pump = { P1 = "cmd-routine-p1", P2 = "cmd-routine-p2" }, - kb7 = { P1 = "default", P2 = "retrobar" }, - ------------------------------------------------------------- - default = { P1 = "default", P2 = "default" } -} - -function RoutineSkinP1() - if RoutineSkins[CurGameName()] then - return RoutineSkins[CurGameName()].P1 - end - return RoutineSkins["default"].P1 -end - -function RoutineSkinP2() - if RoutineSkins[CurGameName()] then - return RoutineSkins[CurGameName()].P2 - end - return RoutineSkins["default"].P2 -end - --- todo: use tables for some of these -aj -function HoldTiming() - return IsGame("pump") and 0 or PREFSMAN:GetPreference("TimingWindowSecondsHold") -end - -function ShowHoldJudgments() - return not IsGame("pump") -end - -local CodeDetectorCodes = { - -- steps - PrevSteps1 = { - default = "", - dance = "Up,Up", - pump = "+UpLeft", - }, - PrevSteps2 = { - default = "MenuUp,MenuUp", - dance = "MenuUp,MenuUp", - pump = "", - }, - NextSteps1 = { - default = "", - dance = "Down,Down", - pump = "+UpRight", - }, - NextSteps2 = { - default = "MenuDown,MenuDown", - dance = "MenuDown,MenuDown", - pump = "", - }, - -- group - NextGroup = { - default = "", - dance = "MenuUp,MenuRight,MenuRight", - pump = "", - }, - PrevGroup = { - default = "", - dance = "MenuUp,MenuDown,MenuUp,MenuDown", - pump = "", - }, - CloseCurrentFolder = { - default = "MenuUp-MenuDown", - }, - -- sorts - NextSort1 = { - default = "@MenuLeft-@MenuRight-Start", - dance = "@MenuLeft-@MenuRight-Start", - pump = "@MenuLeft-@MenuRight-Start", - }, - NextSort2 = { - default = "MenuLeft-MenuRight", - dance = "MenuLeft-MenuRight", - pump = "MenuLeft-MenuRight", - }, - NextSort3 = { - default = "", - dance = "@Left-@Right-Start", - pump = "@DownLeft-@DownRight-Start", - }, - NextSort4 = { - default = "", - dance = "Left-Right", - pump = "DownLeft-DownRight", - }, - -- modemenu - ModeMenu1 = { - default = "", - dance = "Up,Down,Up,Down", - }, - ModeMenu2 = { - default = "MenuUp,MenuDown,MenuUp,MenuDown", - }, - -- Evaluation: - SaveScreenshot1 = { - default = "MenuLeft-MenuRight", - }, - SaveScreenshot2 = { - default = "Select", - }, - -- modifiers section - CancelAll = { - default = "", - dance = "Left,Right,Left,Right,Left,Right,Left,Right", - }, - --- specific modifiers - Mirror = { - default = "", - dance = "Up,Left,Right,Left,Right", - pump = "DownRight,DownLeft,UpRight,UpLeft,DownRight,DownLeft,UpRight,UpLeft,Center", - }, - Left = { - default = "", - dance = "Up,Down,Right,Left", - }, - Right = { - default = "", - dance = "Up,Down,Left,Right", - }, - Shuffle = { - default = "", - dance = "Down,Up,Down,Up", - pump = "UpLeft,UpRight,UpLeft,UpRight,DownLeft,DownRight,DownLeft,DownRight,Center", -- random - }, - SuperShuffle = { - default = "", - dance = "Down,Up,Left,Right", - pump = "UpLeft,UpRight,DownLeft,DownRight,UpLeft,UpRight,DownLeft,DownRight,Center" - }, - Reverse = { - default = "", - dance = "Down,Left,Right,Left,Right", - pump = "UpLeft,DownLeft,UpRight,DownRight,UpLeft,DownLeft,UpRight,DownRight,DownRight", -- drop - }, - HoldNotes = { - default = "", - dance = "Right,Left,Down,Up", - }, - Mines = { - default = "", - }, - Dark = { - default = "", - }, - Hidden = { - default = "", - pump = "UpLeft,UpRight,DownLeft,DownRight,Center", -- vanish - }, - RandomVanish = { - default = "", - }, - -- boost (accel), brake (decel), stealth (nonstep) - --- next/prev modifiers - NextTransform = { - default = "", - }, - NextScrollSpeed = { - default = "", - dance = "Up,Left,Down,Left,Up", - pump = "UpLeft,UpRight,UpLeft,UpRight,Center", - }, - PreviousScrollSpeed = { - default = "", - dance = "Down,Right,Up,Right,Down", - pump = "UpRight,UpLeft,UpRight,UpLeft,Center", - }, - NextAccel = { - default = "", - dance = "Left,Right,Down,Up", - }, - NextEffect = { - default = "", - dance = "Left,Down,Right", - }, - NextAppearance = { - default = "", - dance = "Left,Up,Right", - }, - NextTurn = { - default = "", - }, - -- cancel all in player options - CancelAllPlayerOptions = { - default = "", - dance = "Left,Right,Left,Right,Left,Right", - }, -}; - -function GetCodeForGame(codeName) - local gameName = string.lower(CurGameName()) - local inputCode = CodeDetectorCodes[codeName] - return inputCode[gameName] or inputCode["default"] -end +-- sm-ssc fallback theme | script ring 03 | Gameplay.lua +-- [en] This file is used to store settings that should be different in each +-- game mode. + +-- shakesoda calls this pump.lua +local function CurGameName() + return GAMESTATE:GetCurrentGame():GetName() +end + +-- Check the active game mode against a string. Cut down typing this in metrics. +function IsGame(str) return CurGameName() == str end + +-- GetExtraColorThreshold() +-- [en] returns the difficulty threshold in meter +-- for songs that should be counted as boss songs. +function GetExtraColorThreshold() + local Modes = { + dance = 10, + pump = 21, + beat = 12, + kb7 = 10, + para = 10, + techno = 10, + lights = 10, -- lights shouldn't be playable + } + return Modes[CurGameName()] +end + +-- AllowOptionsMenu() +-- [en] returns if you are able to select options +-- on ScreenSelectMusic. +function AllowOptionsMenu() + if GAMESTATE:IsAnExtraStage() then + return false + elseif GAMESTATE:GetPlayMode() == "PlayMode_Oni" then + return false + else + return true + end +end + +-- GameCompatibleModes: +-- [en] returns possible modes for ScreenSelectPlayMode +function GameCompatibleModes() + local Modes = { + dance = "Single,Double,Solo,Versus,Couple", + pump = "Single,Double,HalfDouble,Versus,Couple,Routine", + beat = "5Keys,7Keys,10Keys,14Keys,Versus5,Versus7", + kb7 = "KB7", + para = "Single", + -- todo: add versus modes for technomotion + techno = "Single4,Single5,Single8,Double4,Double5,Double8", + lights = "Single" -- lights shouldn't be playable + } + return Modes[CurGameName()] +end + +function SelectProfileKeys() + local sGame = CurGameName() + if sGame == "pump" then + return "Up,Down,Start,Back,Center,DownLeft,DownRight" + elseif sGame == "dance" then + return "Up,Down,Start,Back,Up2,Down2" + else + return "Up,Down,Start,Back" + end +end + +-- ScoreKeeperClass: +-- [en] Determines the correct ScoreKeeper class to use. +function ScoreKeeperClass() + -- rave scorekeeper + if GAMESTATE:GetPlayMode() == 'PlayMode_Rave' then return "ScoreKeeperRave" end + if GAMESTATE:GetCurrentStyle() then + local styleType = GAMESTATE:GetCurrentStyle():GetStyleType() + if styleType == 'StyleType_TwoPlayersSharedSides' then return "ScoreKeeperShared" end + end + return "ScoreKeeperNormal" +end + +-- ComboContinue: +-- [en] +function ComboContinue() + local Continue = { + dance = GAMESTATE:GetPlayMode() == "PlayMode_Oni" and "TapNoteScore_W2" or "TapNoteScore_W3", + pump = "TapNoteScore_W3", + beat = "TapNoteScore_W3", + kb7 = "TapNoteScore_W3", + para = "TapNoteScore_W4" + } + return Continue[CurGameName()] +end + +function ComboMaintain() + local Maintain = { + dance = "TapNoteScore_W3", + pump = "TapNoteScore_W4", + beat = "TapNoteScore_W3", + kb7 = "TapNoteScore_W3", + para = "TapNoteScore_W4" + } + return Maintain[CurGameName()] +end + +function ComboPerRow() + sGame = CurGameName() + if sGame == "pump" then + return true + elseif GAMESTATE:GetPlayMode() == "PlayMode_Oni" then + return true + else + return false + end +end + +function EvalUsesCheckpointsWithJudgments() + return (CurGameName() == "pump") and true or false +end + +local ComboThresholds = { + dance = { Hit = 2, Miss = 2, Fail = -1 }, + pump = { Hit = 4, Miss = 4, Fail = 51 }, + beat = { Hit = 1, Miss = 0, Fail = -1 }, + kb7 = { Hit = 1, Miss = 0, Fail = -1 }, + para = { Hit = 2, Miss = 0, Fail = -1 }, + maniax = { Hit = 5, Miss = 0, Fail = -1 }, + ------------------------------------------- + default = { Hit = 2, Miss = 2, Fail = -1 } +} + +function HitCombo() + if ComboThresholds[CurGameName()] then + return ComboThresholds[CurGameName()].Hit + end + return ComboThresholds["default"].Hit +end + +function MissCombo() + if ComboThresholds[CurGameName()] then + return ComboThresholds[CurGameName()].Miss + end + return ComboThresholds["default"].Miss +end + +-- FailCombo: +-- [en] The combo that causes game failure. +function FailCombo() + -- ITG (dance) uses 30. Pump Pro uses 30, real Pump uses 51 + if ComboThresholds[CurGameName()] then + return ComboThresholds[CurGameName()].Fail + end + return ComboThresholds["default"].Fail +end + +local RoutineSkins = { + dance = { P1 = "midi-routine-p1", P2 = "midi-routine-p2" }, + pump = { P1 = "cmd-routine-p1", P2 = "cmd-routine-p2" }, + kb7 = { P1 = "default", P2 = "retrobar" }, + ------------------------------------------------------------- + default = { P1 = "default", P2 = "default" } +} + +function RoutineSkinP1() + if RoutineSkins[CurGameName()] then + return RoutineSkins[CurGameName()].P1 + end + return RoutineSkins["default"].P1 +end + +function RoutineSkinP2() + if RoutineSkins[CurGameName()] then + return RoutineSkins[CurGameName()].P2 + end + return RoutineSkins["default"].P2 +end + +-- todo: use tables for some of these -aj +function HoldTiming() + return IsGame("pump") and 0 or PREFSMAN:GetPreference("TimingWindowSecondsHold") +end + +function ShowHoldJudgments() + return not IsGame("pump") +end + +local CodeDetectorCodes = { + -- steps + PrevSteps1 = { + default = "", + dance = "Up,Up", + pump = "+UpLeft", + }, + PrevSteps2 = { + default = "MenuUp,MenuUp", + dance = "MenuUp,MenuUp", + pump = "", + }, + NextSteps1 = { + default = "", + dance = "Down,Down", + pump = "+UpRight", + }, + NextSteps2 = { + default = "MenuDown,MenuDown", + dance = "MenuDown,MenuDown", + pump = "", + }, + -- group + NextGroup = { + default = "", + dance = "MenuUp,MenuRight,MenuRight", + pump = "", + }, + PrevGroup = { + default = "", + dance = "MenuUp,MenuDown,MenuUp,MenuDown", + pump = "", + }, + CloseCurrentFolder = { + default = "MenuUp-MenuDown", + }, + -- sorts + NextSort1 = { + default = "@MenuLeft-@MenuRight-Start", + dance = "@MenuLeft-@MenuRight-Start", + pump = "@MenuLeft-@MenuRight-Start", + }, + NextSort2 = { + default = "MenuLeft-MenuRight", + dance = "MenuLeft-MenuRight", + pump = "MenuLeft-MenuRight", + }, + NextSort3 = { + default = "", + dance = "@Left-@Right-Start", + pump = "@DownLeft-@DownRight-Start", + }, + NextSort4 = { + default = "", + dance = "Left-Right", + pump = "DownLeft-DownRight", + }, + -- modemenu + ModeMenu1 = { + default = "", + dance = "Up,Down,Up,Down", + }, + ModeMenu2 = { + default = "MenuUp,MenuDown,MenuUp,MenuDown", + }, + -- Evaluation: + SaveScreenshot1 = { + default = "MenuLeft-MenuRight", + }, + SaveScreenshot2 = { + default = "Select", + }, + -- modifiers section + CancelAll = { + default = "", + dance = "Left,Right,Left,Right,Left,Right,Left,Right", + }, + --- specific modifiers + Mirror = { + default = "", + dance = "Up,Left,Right,Left,Right", + pump = "DownRight,DownLeft,UpRight,UpLeft,DownRight,DownLeft,UpRight,UpLeft,Center", + }, + Left = { + default = "", + dance = "Up,Down,Right,Left", + }, + Right = { + default = "", + dance = "Up,Down,Left,Right", + }, + Shuffle = { + default = "", + dance = "Down,Up,Down,Up", + pump = "UpLeft,UpRight,UpLeft,UpRight,DownLeft,DownRight,DownLeft,DownRight,Center", -- random + }, + SuperShuffle = { + default = "", + dance = "Down,Up,Left,Right", + pump = "UpLeft,UpRight,DownLeft,DownRight,UpLeft,UpRight,DownLeft,DownRight,Center" + }, + Reverse = { + default = "", + dance = "Down,Left,Right,Left,Right", + pump = "UpLeft,DownLeft,UpRight,DownRight,UpLeft,DownLeft,UpRight,DownRight,DownRight", -- drop + }, + HoldNotes = { + default = "", + dance = "Right,Left,Down,Up", + }, + Mines = { + default = "", + }, + Dark = { + default = "", + }, + Hidden = { + default = "", + pump = "UpLeft,UpRight,DownLeft,DownRight,Center", -- vanish + }, + RandomVanish = { + default = "", + }, + -- boost (accel), brake (decel), stealth (nonstep) + --- next/prev modifiers + NextTransform = { + default = "", + }, + NextScrollSpeed = { + default = "", + dance = "Up,Left,Down,Left,Up", + pump = "UpLeft,UpRight,UpLeft,UpRight,Center", + }, + PreviousScrollSpeed = { + default = "", + dance = "Down,Right,Up,Right,Down", + pump = "UpRight,UpLeft,UpRight,UpLeft,Center", + }, + NextAccel = { + default = "", + dance = "Left,Right,Down,Up", + }, + NextEffect = { + default = "", + dance = "Left,Down,Right", + }, + NextAppearance = { + default = "", + dance = "Left,Up,Right", + }, + NextTurn = { + default = "", + }, + -- cancel all in player options + CancelAllPlayerOptions = { + default = "", + dance = "Left,Right,Left,Right,Left,Right", + }, +}; + +function GetCodeForGame(codeName) + local gameName = string.lower(CurGameName()) + local inputCode = CodeDetectorCodes[codeName] + return inputCode[gameName] or inputCode["default"] +end diff --git a/Themes/_fallback/metrics.ini b/Themes/_fallback/metrics.ini index 3467666af3..ff96f720be 100644 --- a/Themes/_fallback/metrics.ini +++ b/Themes/_fallback/metrics.ini @@ -1322,6 +1322,24 @@ LevelP2Y= LevelP2OnCommand= LevelP2OffCommand= +FrameBaseP1X= +FrameBaseP1Y= +FrameBaseP1OnCommand= +FrameBaseP1OffCommand= +FrameBaseP2X= +FrameBaseP2Y= +FrameBaseP2OnCommand= +FrameBaseP2OffCommand= + +FrameOverP1X= +FrameOverP1Y= +FrameOverP1OnCommand= +FrameOverP1OffCommand= +FrameOverP2X= +FrameOverP2Y= +FrameOverP2OnCommand= +FrameOverP2OffCommand= + [ScoreKeeperRave] AttackDurationSeconds=3.0 @@ -3822,7 +3840,7 @@ ShowCreditDisplay=false ShowStyleIcon=false InvertScrollSpeedButtons=false TimerSeconds=-1 -EditModifiers="" +EditModifiers="no reverse" EditHelpX=SCREEN_LEFT+4 EditHelpY=SCREEN_TOP+16 EditHelpOnCommand=halign,0;valign,0;zoom,0.5;shadowlength,1 @@ -3923,7 +3941,7 @@ ShowStyleIcon=false LineNames="1,2,3,4,5,6,R,7,8,9,10,Attacks,11,12,13,14,15,16" # uses legacy speed line -Line1="lua,SpeedMods()" +Line1="list,Speed" Line2="list,Accel" Line3="list,Effect" Line4="list,Appearance" @@ -4457,6 +4475,7 @@ Class="ScreenAttract" NextScreen="ScreenInit" PrevScreen="ScreenInit" TimerSeconds=3600 +TimerStealth=true ShowCreditDisplay=false [ScreenTitleJoin] diff --git a/Themes/default/Other/ScreenGameplaySyncMachine music.sm b/Themes/default/Other/ScreenGameplaySyncMachine music.sm index f04a555f23..01dea4b201 100644 --- a/Themes/default/Other/ScreenGameplaySyncMachine music.sm +++ b/Themes/default/Other/ScreenGameplaySyncMachine music.sm @@ -11,11 +11,11 @@ #LYRICSPATH:; #CDTITLE:; #MUSIC:_gameplay sync music.ogg; -#OFFSET:-0.012; +#OFFSET:-0.025; #SAMPLESTART:0.000; #SAMPLELENGTH:12.000; #SELECTABLE:YES; -#BPMS:0.000=132.000; +#BPMS:0.000=132.027; #STOPS:; #BGCHANGES:; #KEYSOUNDS:; diff --git a/src/CreateZip.cpp b/src/CreateZip.cpp new file mode 100644 index 0000000000..9a26ff45e4 --- /dev/null +++ b/src/CreateZip.cpp @@ -0,0 +1,1032 @@ +#include "global.h" + +#include + +#if defined(_WINDOWS) +#include +#else +#define _tcslen strlen +#define _tcscpy strcpy +typedef char TCHAR; +#endif + +#include +#include "CreateZip.h" +#include "RageFile.h" +#include "RageUtil.h" + +#define MAX_PATH 1024 + +// TODO: remove header "extra fields" (the 3 dates) +// Adapted for StepMania from http://www.codeproject.com/KB/files/zip_utils.aspx +// +// THIS FILE is almost entirely based upon code by info-zip. +// It has been modified by Lucian Wischik. The modifications +// were a complete rewrite of the bit of code that generates the +// layout of the zipfile, and support for zipping to/from memory +// or handles or pipes or pagefile or diskfiles, encryption, unicode. +// The original code may be found at http://www.info-zip.org +// The original copyright text follows. +// +// +// +// This is version 1999-Oct-05 of the Info-ZIP copyright and license. +// The definitive version of this document should be available at +// ftp://ftp.cdrom.com/pub/infozip/license.html indefinitely. +// +// Copyright (c) 1990-1999 Info-ZIP. All rights reserved. +// +// For the purposes of this copyright and license, "Info-ZIP" is defined as +// the following set of individuals: +// +// Mark Adler, John Bush, Karl Davis, Harald Denker, Jean-Michel Dubois, +// Jean-loup Gailly, Hunter Goatley, Ian Gorman, Chris Herborth, Dirk Haase, +// Greg Hartwig, Robert Heath, Jonathan Hudson, Paul Kienitz, David Kirschbaum, +// Johnny Lee, Onno van der Linden, Igor Mandrichenko, Steve P. Miller, +// Sergio Monesi, Keith Owens, George Petrov, Greg Roelofs, Kai Uwe Rommel, +// Steve Salisbury, Dave Smith, Christian Spieler, Antoine Verheijen, +// Paul von Behren, Rich Wales, Mike White +// +// This software is provided "as is," without warranty of any kind, express +// or implied. In no event shall Info-ZIP or its contributors be held liable +// for any direct, indirect, incidental, special or consequential damages +// arising out of the use of or inability to use this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it +// freely, subject to the following restrictions: +// +// 1. Redistributions of source code must retain the above copyright notice, +// definition, disclaimer, and this list of conditions. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, definition, disclaimer, and this list of conditions in +// documentation and/or other materials provided with the distribution. +// +// 3. Altered versions--including, but not limited to, ports to new operating +// systems, existing ports with new graphical interfaces, and dynamic, +// shared, or static library versions--must be plainly marked as such +// and must not be misrepresented as being the original source. Such +// altered versions also must not be misrepresented as being Info-ZIP +// releases--including, but not limited to, labeling of the altered +// versions with the names "Info-ZIP" (or any variation thereof, including, +// but not limited to, different capitalizations), "Pocket UnZip," "WiZ" +// or "MacZip" without the explicit permission of Info-ZIP. Such altered +// versions are further prohibited from misrepresentative use of the +// Zip-Bugs or Info-ZIP e-mail addresses or of the Info-ZIP URL(s). +// +// 4. Info-ZIP retains the right to use the names "Info-ZIP," "Zip," "UnZip," +// "WiZ," "Pocket UnZip," "Pocket Zip," and "MacZip" for its own source and +// binary releases. +// + + + +// These are the result codes: +#define ZR_OK 0x00000000 // nb. the pseudo-code zr-recent is never returned, +#define ZR_RECENT 0x00000001 // but can be passed to FormatZipMessage. +// The following come from general system stuff (e.g. files not openable) +#define ZR_GENMASK 0x0000FF00 +#define ZR_NODUPH 0x00000100 // couldn't duplicate the handle +#define ZR_NOFILE 0x00000200 // couldn't create/open the file +#define ZR_NOALLOC 0x00000300 // failed to allocate some resource +#define ZR_WRITE 0x00000400 // a general error writing to the file +#define ZR_NOTFOUND 0x00000500 // couldn't find that file in the zip +#define ZR_MORE 0x00000600 // there's still more data to be unzipped +#define ZR_CORRUPT 0x00000700 // the zipfile is corrupt or not a zipfile +#define ZR_READ 0x00000800 // a general error reading the file +// The following come from mistakes on the part of the caller +#define ZR_CALLERMASK 0x00FF0000 +#define ZR_ARGS 0x00010000 // general mistake with the arguments +#define ZR_MEMSIZE 0x00030000 // the memory size is too small +#define ZR_FAILED 0x00040000 // the thing was already failed when you called this function +#define ZR_ENDED 0x00050000 // the zip creation has already been closed +#define ZR_MISSIZE 0x00060000 // the indicated input file size turned out mistaken +#define ZR_PARTIALUNZ 0x00070000 // the file had already been partially unzipped +#define ZR_ZMODE 0x00080000 // tried to mix creating/opening a zip +// The following come from bugs within the zip library itself +#define ZR_BUGMASK 0xFF000000 +#define ZR_NOTINITED 0x01000000 // initialisation didn't work +#define ZR_SEEK 0x02000000 // trying to seek in an unseekable file +#define ZR_NOCHANGE 0x04000000 // changed its mind on storage, but not allowed +#define ZR_FLATE 0x05000000 // an internal error in the de/inflation code + + RString FormatZipMessageZ(ZRESULT code) + { + const char *msg="unknown zip result code"; + switch (code) + { + case ZR_OK: msg="Success"; break; + case ZR_NODUPH: msg="Culdn't duplicate handle"; break; + case ZR_NOFILE: msg="Couldn't create/open file"; break; + case ZR_NOALLOC: msg="Failed to allocate memory"; break; + case ZR_WRITE: msg="Error writing to file"; break; + case ZR_NOTFOUND: msg="File not found in the zipfile"; break; + case ZR_MORE: msg="Still more data to unzip"; break; + case ZR_CORRUPT: msg="Zipfile is corrupt or not a zipfile"; break; + case ZR_READ: msg="Error reading file"; break; + case ZR_ARGS: msg="Caller: faulty arguments"; break; + case ZR_PARTIALUNZ: msg="Caller: the file had already been partially unzipped"; break; + case ZR_MEMSIZE: msg="Caller: not enough space allocated for memory zipfile"; break; + case ZR_FAILED: msg="Caller: there was a previous error"; break; + case ZR_ENDED: msg="Caller: additions to the zip have already been ended"; break; + case ZR_ZMODE: msg="Caller: mixing creation and opening of zip"; break; + case ZR_NOTINITED: msg="Zip-bug: internal initialisation not completed"; break; + case ZR_SEEK: msg="Zip-bug: trying to seek the unseekable"; break; + case ZR_MISSIZE: msg="Zip-bug: the anticipated size turned out wrong"; break; + case ZR_NOCHANGE: msg="Zip-bug: tried to change mind, but not allowed"; break; + case ZR_FLATE: msg="Zip-bug: an internal error during flation"; break; + } + return msg; + } + + +typedef unsigned char uch; // unsigned 8-bit value +typedef unsigned short ush; // unsigned 16-bit value +typedef unsigned long ulg; // unsigned 32-bit value +typedef size_t extent; // file size +typedef unsigned Pos; // must be at least 32 bits +typedef unsigned IPos; // A Pos is an index in the character window. Pos is used only for parameter passing + +#ifndef EOF +#define EOF (-1) +#endif + + +// Error return values. The values 0..4 and 12..18 follow the conventions +// of PKZIP. The values 4..10 are all assigned to "insufficient memory" +// by PKZIP, so the codes 5..10 are used here for other purposes. +#define ZE_MISS -1 // used by procname(), zipbare() +#define ZE_OK 0 // success +#define ZE_EOF 2 // unexpected end of zip file +#define ZE_FORM 3 // zip file structure error +#define ZE_MEM 4 // out of memory +#define ZE_LOGIC 5 // internal logic error +#define ZE_BIG 6 // entry too large to split +#define ZE_NOTE 7 // invalid comment format +#define ZE_TEST 8 // zip test (-T) failed or out of memory +#define ZE_ABORT 9 // user interrupt or termination +#define ZE_TEMP 10 // error using a temp file +#define ZE_READ 11 // read or seek error +#define ZE_NONE 12 // nothing to do +#define ZE_NAME 13 // missing or empty zip file +#define ZE_WRITE 14 // error writing to a file +#define ZE_CREAT 15 // couldn't open to write +#define ZE_PARMS 16 // bad command line +#define ZE_OPEN 18 // could not open a specified file to read +#define ZE_MAXERR 18 // the highest error number + + +// internal file attribute +#define UNKNOWN (-1) +#define BINARY 0 +#define ASCII 1 + +#define STORE 0 // Store method + +#define CRCVAL_INITIAL 0L + +// MSDOS file or directory attributes +#define MSDOS_HIDDEN_ATTR 0x02 +#define MSDOS_DIR_ATTR 0x10 + +// Lengths of headers after signatures in bytes +#define LOCHEAD 26 +#define CENHEAD 42 +#define ENDHEAD 18 + +// Definitions for extra field handling: +#define EB_HEADSIZE 4 /* length of a extra field block header */ +#define EB_LEN 2 /* offset of data length field in header */ +#define EB_UT_MINLEN 1 /* minimal UT field contains Flags byte */ +#define EB_UT_FLAGS 0 /* byte offset of Flags field */ +#define EB_UT_TIME1 1 /* byte offset of 1st time value */ +#define EB_UT_FL_MTIME (1 << 0) /* mtime present */ +#define EB_UT_FL_ATIME (1 << 1) /* atime present */ +#define EB_UT_FL_CTIME (1 << 2) /* ctime present */ +#define EB_UT_LEN(n) (EB_UT_MINLEN + 4 * (n)) +#define EB_L_UT_SIZE (EB_HEADSIZE + EB_UT_LEN(3)) +#define EB_C_UT_SIZE (EB_HEADSIZE + EB_UT_LEN(1)) + + +// Macros for writing machine integers to little-endian format +#define PUTSH(a,f) {char _putsh_c=(char)((a)&0xff); wfunc(param,&_putsh_c,1); _putsh_c=(char)((a)>>8); wfunc(param,&_putsh_c,1);} +#define PUTLG(a,f) {PUTSH((a) & 0xffff,(f)) PUTSH((a) >> 16,(f))} + + +// -- Structure of a ZIP file -- +// Signatures for zip file information headers +#define LOCSIG 0x04034b50L +#define CENSIG 0x02014b50L +#define ENDSIG 0x06054b50L +#define EXTLOCSIG 0x08074b50L + + +#define MIN_MATCH 3 +#define MAX_MATCH 258 +// The minimum and maximum match lengths + + +#define WSIZE (0x8000) +// Maximum window size = 32K. If you are really short of memory, compile +// with a smaller WSIZE but this reduces the compression ratio for files +// of size > WSIZE. WSIZE must be a power of two in the current implementation. +// + +#define MIN_LOOKAHEAD (MAX_MATCH+MIN_MATCH+1) +// Minimum amount of lookahead, except at the end of the input file. +// See deflate.c for comments about the MIN_MATCH+1. +// + +#define MAX_DIST (WSIZE-MIN_LOOKAHEAD) +// In order to simplify the code, particularly on 16 bit machines, match +// distances are limited to MAX_DIST instead of WSIZE. +// + + +#define ZIP_FILENAME 2 +#define ZIP_FOLDER 4 + + + +// =========================================================================== +// Constants +// + +#define MAX_BITS 15 +// All codes must not exceed MAX_BITS bits + +#define MAX_BL_BITS 7 +// Bit length codes must not exceed MAX_BL_BITS bits + +#define LENGTH_CODES 29 +// number of length codes, not counting the special END_BLOCK code + +#define LITERALS 256 +// number of literal bytes 0..255 + +#define END_BLOCK 256 +// end of block literal code + +#define L_CODES (LITERALS+1+LENGTH_CODES) +// number of Literal or Length codes, including the END_BLOCK code + +#define D_CODES 30 +// number of distance codes + +#define BL_CODES 19 +// number of codes used to transfer the bit lengths + + +#define LIT_BUFSIZE 0x8000 +#define DIST_BUFSIZE LIT_BUFSIZE +// Sizes of match buffers for literals/lengths and distances. There are +// 4 reasons for limiting LIT_BUFSIZE to 64K: +// - frequencies can be kept in 16 bit counters +// - if compression is not successful for the first block, all input data is +// still in the window so we can still emit a stored block even when input +// comes from standard input. (This can also be done for all blocks if +// LIT_BUFSIZE is not greater than 32K.) +// - if compression is not successful for a file smaller than 64K, we can +// even emit a stored file instead of a stored block (saving 5 bytes). +// - creating new Huffman trees less frequently may not provide fast +// adaptation to changes in the input data statistics. (Take for +// example a binary file with poorly compressible code followed by +// a highly compressible string table.) Smaller buffer sizes give +// fast adaptation but have of course the overhead of transmitting trees +// more frequently. +// - I can't count above 4 +// The current code is general and allows DIST_BUFSIZE < LIT_BUFSIZE (to save +// memory at the expense of compression). Some optimizations would be possible +// if we rely on DIST_BUFSIZE == LIT_BUFSIZE. +// + +#define REP_3_6 16 +// repeat previous bit length 3-6 times (2 bits of repeat count) + +#define REPZ_3_10 17 +// repeat a zero length 3-10 times (3 bits of repeat count) + +#define REPZ_11_138 18 +// repeat a zero length 11-138 times (7 bits of repeat count) + +#define HEAP_SIZE (2*L_CODES+1) +// maximum heap size + + +// =========================================================================== +// Local data used by the "bit string" routines. +// + +#define Buf_size (8 * 2*sizeof(char)) +// Number of bits used within bi_buf. (bi_buf may be implemented on +// more than 16 bits on some systems.) + +// Output a 16 bit value to the bit stream, lower (oldest) byte first +#define PUTSHORT(state,w) \ +{ if (state.bs.out_offset >= state.bs.out_size-1) \ + state.flush_outbuf(state.param,state.bs.out_buf, &state.bs.out_offset); \ + state.bs.out_buf[state.bs.out_offset++] = (char) ((w) & 0xff); \ + state.bs.out_buf[state.bs.out_offset++] = (char) ((ush)(w) >> 8); \ +} + +#define PUTBYTE(state,b) \ +{ if (state.bs.out_offset >= state.bs.out_size) \ + state.flush_outbuf(state.param,state.bs.out_buf, &state.bs.out_offset); \ + state.bs.out_buf[state.bs.out_offset++] = (char) (b); \ +} + + + +typedef int64_t lutime_t; // define it ourselves since we don't include time.h + +typedef struct iztimes { + lutime_t atime,mtime,ctime; +} iztimes; // access, modify, create times + +typedef struct zlist { + ush vem, ver, flg, how; // See central header in zipfile.c for what vem..off are + ulg tim, crc, siz, len; + size_t nam, ext, cext, com; // offset of ext must be >= LOCHEAD + ush dsk, att, lflg; // offset of lflg must be >= LOCHEAD + ulg atx, off; + char name[MAX_PATH]; // File name in zip file + char *extra; // Extra field (set only if ext != 0) + char *cextra; // Extra in central (set only if cext != 0) + char *comment; // Comment (set only if com != 0) + char iname[MAX_PATH]; // Internal file name after cleanup + char zname[MAX_PATH]; // External version of internal name + int mark; // Marker for files to operate on + int trash; // Marker for files to delete + int dosflag; // Set to force MSDOS file attributes + struct zlist *nxt; // Pointer to next header in list +} TZipFileInfo; + + + + + +/* +void __cdecl Trace(const char *x, ...) {va_list paramList; va_start(paramList, x); paramList; va_end(paramList);} +void __cdecl Tracec(bool ,const char *x, ...) {va_list paramList; va_start(paramList, x); paramList; va_end(paramList);} +*/ + + + +struct TState; +typedef unsigned (*READFUNC)(TState &state, char *buf,unsigned size); +typedef unsigned (*FLUSHFUNC)(void *param, const char *buf, unsigned *size); +typedef unsigned (*WRITEFUNC)(void *param, const char *buf, unsigned size); +struct TState +{ + void *param; + int level; bool seekable; + READFUNC readfunc; FLUSHFUNC flush_outbuf; + const char *err; +}; + + + +int putlocal(struct zlist *z, WRITEFUNC wfunc,void *param) +{ + // Write a local header described by *z to file *f. Return a ZE_ error code. + PUTLG(LOCSIG, f); + PUTSH(z->ver, f); + PUTSH(z->lflg, f); + PUTSH(z->how, f); + PUTLG(z->tim, f); + PUTLG(z->crc, f); + PUTLG(z->siz, f); + PUTLG(z->len, f); + PUTSH(z->nam, f); + PUTSH(z->ext, f); + size_t res = (size_t)wfunc(param, z->iname, (unsigned int)z->nam); + if (res!=z->nam) + return ZE_TEMP; + if (z->ext) + { + res = (size_t)wfunc(param, z->extra, (unsigned int)z->ext); + if (res!=z->ext) + return ZE_TEMP; + } + return ZE_OK; +} + +int putextended(struct zlist *z, WRITEFUNC wfunc, void *param) +{ + // Write an extended local header described by *z to file *f. Returns a ZE_ code + PUTLG(EXTLOCSIG, f); + PUTLG(z->crc, f); + PUTLG(z->siz, f); + PUTLG(z->len, f); + return ZE_OK; +} + +int putcentral(struct zlist *z, WRITEFUNC wfunc, void *param) +{ + // Write a central header entry of *z to file *f. Returns a ZE_ code. + PUTLG(CENSIG, f); + PUTSH(z->vem, f); + PUTSH(z->ver, f); + PUTSH(z->flg, f); + PUTSH(z->how, f); + PUTLG(z->tim, f); + PUTLG(z->crc, f); + PUTLG(z->siz, f); + PUTLG(z->len, f); + PUTSH(z->nam, f); + PUTSH(z->cext, f); + PUTSH(z->com, f); + PUTSH(z->dsk, f); + PUTSH(z->att, f); + PUTLG(z->atx, f); + PUTLG(z->off, f); + if ((size_t)wfunc(param, z->iname, (unsigned int)z->nam) != z->nam || + (z->cext && (size_t)wfunc(param, z->cextra, (unsigned int)z->cext) != z->cext) || + (z->com && (size_t)wfunc(param, z->comment, (unsigned int)z->com) != z->com)) + return ZE_TEMP; + return ZE_OK; +} + + +int putend(int n, ulg s, ulg c, size_t m, char *z, WRITEFUNC wfunc, void *param) +{ + // write the end of the central-directory-data to file *f. + PUTLG(ENDSIG, f); + PUTSH(0, f); + PUTSH(0, f); + PUTSH(n, f); + PUTSH(n, f); + PUTLG(s, f); + PUTLG(c, f); + PUTSH(m, f); + // Write the comment, if any + if (m && wfunc(param, z, (unsigned int)m) != m) return ZE_TEMP; + return ZE_OK; +} + + + + + +const ulg crc_table[256] = { + 0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L, + 0x706af48fL, 0xe963a535L, 0x9e6495a3L, 0x0edb8832L, 0x79dcb8a4L, + 0xe0d5e91eL, 0x97d2d988L, 0x09b64c2bL, 0x7eb17cbdL, 0xe7b82d07L, + 0x90bf1d91L, 0x1db71064L, 0x6ab020f2L, 0xf3b97148L, 0x84be41deL, + 0x1adad47dL, 0x6ddde4ebL, 0xf4d4b551L, 0x83d385c7L, 0x136c9856L, + 0x646ba8c0L, 0xfd62f97aL, 0x8a65c9ecL, 0x14015c4fL, 0x63066cd9L, + 0xfa0f3d63L, 0x8d080df5L, 0x3b6e20c8L, 0x4c69105eL, 0xd56041e4L, + 0xa2677172L, 0x3c03e4d1L, 0x4b04d447L, 0xd20d85fdL, 0xa50ab56bL, + 0x35b5a8faL, 0x42b2986cL, 0xdbbbc9d6L, 0xacbcf940L, 0x32d86ce3L, + 0x45df5c75L, 0xdcd60dcfL, 0xabd13d59L, 0x26d930acL, 0x51de003aL, + 0xc8d75180L, 0xbfd06116L, 0x21b4f4b5L, 0x56b3c423L, 0xcfba9599L, + 0xb8bda50fL, 0x2802b89eL, 0x5f058808L, 0xc60cd9b2L, 0xb10be924L, + 0x2f6f7c87L, 0x58684c11L, 0xc1611dabL, 0xb6662d3dL, 0x76dc4190L, + 0x01db7106L, 0x98d220bcL, 0xefd5102aL, 0x71b18589L, 0x06b6b51fL, + 0x9fbfe4a5L, 0xe8b8d433L, 0x7807c9a2L, 0x0f00f934L, 0x9609a88eL, + 0xe10e9818L, 0x7f6a0dbbL, 0x086d3d2dL, 0x91646c97L, 0xe6635c01L, + 0x6b6b51f4L, 0x1c6c6162L, 0x856530d8L, 0xf262004eL, 0x6c0695edL, + 0x1b01a57bL, 0x8208f4c1L, 0xf50fc457L, 0x65b0d9c6L, 0x12b7e950L, + 0x8bbeb8eaL, 0xfcb9887cL, 0x62dd1ddfL, 0x15da2d49L, 0x8cd37cf3L, + 0xfbd44c65L, 0x4db26158L, 0x3ab551ceL, 0xa3bc0074L, 0xd4bb30e2L, + 0x4adfa541L, 0x3dd895d7L, 0xa4d1c46dL, 0xd3d6f4fbL, 0x4369e96aL, + 0x346ed9fcL, 0xad678846L, 0xda60b8d0L, 0x44042d73L, 0x33031de5L, + 0xaa0a4c5fL, 0xdd0d7cc9L, 0x5005713cL, 0x270241aaL, 0xbe0b1010L, + 0xc90c2086L, 0x5768b525L, 0x206f85b3L, 0xb966d409L, 0xce61e49fL, + 0x5edef90eL, 0x29d9c998L, 0xb0d09822L, 0xc7d7a8b4L, 0x59b33d17L, + 0x2eb40d81L, 0xb7bd5c3bL, 0xc0ba6cadL, 0xedb88320L, 0x9abfb3b6L, + 0x03b6e20cL, 0x74b1d29aL, 0xead54739L, 0x9dd277afL, 0x04db2615L, + 0x73dc1683L, 0xe3630b12L, 0x94643b84L, 0x0d6d6a3eL, 0x7a6a5aa8L, + 0xe40ecf0bL, 0x9309ff9dL, 0x0a00ae27L, 0x7d079eb1L, 0xf00f9344L, + 0x8708a3d2L, 0x1e01f268L, 0x6906c2feL, 0xf762575dL, 0x806567cbL, + 0x196c3671L, 0x6e6b06e7L, 0xfed41b76L, 0x89d32be0L, 0x10da7a5aL, + 0x67dd4accL, 0xf9b9df6fL, 0x8ebeeff9L, 0x17b7be43L, 0x60b08ed5L, + 0xd6d6a3e8L, 0xa1d1937eL, 0x38d8c2c4L, 0x4fdff252L, 0xd1bb67f1L, + 0xa6bc5767L, 0x3fb506ddL, 0x48b2364bL, 0xd80d2bdaL, 0xaf0a1b4cL, + 0x36034af6L, 0x41047a60L, 0xdf60efc3L, 0xa867df55L, 0x316e8eefL, + 0x4669be79L, 0xcb61b38cL, 0xbc66831aL, 0x256fd2a0L, 0x5268e236L, + 0xcc0c7795L, 0xbb0b4703L, 0x220216b9L, 0x5505262fL, 0xc5ba3bbeL, + 0xb2bd0b28L, 0x2bb45a92L, 0x5cb36a04L, 0xc2d7ffa7L, 0xb5d0cf31L, + 0x2cd99e8bL, 0x5bdeae1dL, 0x9b64c2b0L, 0xec63f226L, 0x756aa39cL, + 0x026d930aL, 0x9c0906a9L, 0xeb0e363fL, 0x72076785L, 0x05005713L, + 0x95bf4a82L, 0xe2b87a14L, 0x7bb12baeL, 0x0cb61b38L, 0x92d28e9bL, + 0xe5d5be0dL, 0x7cdcefb7L, 0x0bdbdf21L, 0x86d3d2d4L, 0xf1d4e242L, + 0x68ddb3f8L, 0x1fda836eL, 0x81be16cdL, 0xf6b9265bL, 0x6fb077e1L, + 0x18b74777L, 0x88085ae6L, 0xff0f6a70L, 0x66063bcaL, 0x11010b5cL, + 0x8f659effL, 0xf862ae69L, 0x616bffd3L, 0x166ccf45L, 0xa00ae278L, + 0xd70dd2eeL, 0x4e048354L, 0x3903b3c2L, 0xa7672661L, 0xd06016f7L, + 0x4969474dL, 0x3e6e77dbL, 0xaed16a4aL, 0xd9d65adcL, 0x40df0b66L, + 0x37d83bf0L, 0xa9bcae53L, 0xdebb9ec5L, 0x47b2cf7fL, 0x30b5ffe9L, + 0xbdbdf21cL, 0xcabac28aL, 0x53b39330L, 0x24b4a3a6L, 0xbad03605L, + 0xcdd70693L, 0x54de5729L, 0x23d967bfL, 0xb3667a2eL, 0xc4614ab8L, + 0x5d681b02L, 0x2a6f2b94L, 0xb40bbe37L, 0xc30c8ea1L, 0x5a05df1bL, + 0x2d02ef8dL +}; + +#define CRC32(c, b) (crc_table[((int)(c) ^ (b)) & 0xff] ^ ((c) >> 8)) +#define DO1(buf) crc = CRC32(crc, *buf++) +#define DO2(buf) DO1(buf); DO1(buf) +#define DO4(buf) DO2(buf); DO2(buf) +#define DO8(buf) DO4(buf); DO4(buf) + +ulg crc32(ulg crc, const uch *buf, size_t len) +{ + if (buf==NULL) return 0L; + crc = crc ^ 0xffffffffL; + while (len >= 8) {DO8(buf); len -= 8;} + if (len) do {DO1(buf);} while (--len); + return crc ^ 0xffffffffL; // (instead of ~c for 64-bit machines) +} + + +class TZip +{ +public: + TZip() : pfout(NULL),zfis(0),hfin(0),writ(0),oerr(false),hasputcen(false),ooffset(0) + { + } + ~TZip() + { + } + + // These variables say about the file we're writing into + // We can write to pipe, file-by-handle, file-by-name, memory-to-memmapfile + RageFile *pfout; // if valid, we'll write here (for files or pipes) + unsigned ooffset; // for pfout, this is where the pointer was initially + ZRESULT oerr; // did a write operation give rise to an error? + unsigned writ; // how have we written. This is maintained by Add, not write(), to avoid confusion over seeks + unsigned int opos; // current pos in the mmap + unsigned int mapsize; // the size of the map we created + bool hasputcen; // have we yet placed the central directory? + // + TZipFileInfo *zfis; // each file gets added onto this list, for writing the table at the end + + ZRESULT Start(RageFile *f); + static unsigned sflush(void *param,const char *buf, unsigned *size); + static unsigned swrite(void *param,const char *buf, unsigned size); + unsigned int write(const char *buf,unsigned int size); + bool oseek(unsigned int pos); + ZRESULT Close(); + + // some variables to do with the file currently being read: + // I haven't done it object-orientedly here, just put them all + // together, since OO didn't seem to make the design any clearer. + ulg attr; iztimes times; ulg timestamp; // all open_* methods set these + long isize,ired; // size is not set until close() on pips + ulg crc; // crc is not set until close(). iwrit is cumulative + RageFile *hfin; // for input files and pipes + const char *bufin; unsigned int lenin,posin; // for memory + // and a variable for what we've done with the input: (i.e. compressed it!) + ulg csize; // compressed size, set by the compression routines + // and this is used by some of the compression routines + char buf[16384]; + + + ZRESULT open_file(const TCHAR *fn); + ZRESULT open_dir(); + ZRESULT set_times(); + unsigned read(char *buf, unsigned size); + ZRESULT iclose(); + + ZRESULT ideflate(TZipFileInfo *zfi); + ZRESULT istore(); + + ZRESULT Add(const TCHAR *odstzn, const TCHAR *src, unsigned long flags); + ZRESULT AddCentral(); + +}; + + + +ZRESULT TZip::Start(RageFile *f) +{ + if (pfout!=0 || writ!=0 || oerr!=ZR_OK || hasputcen) + return ZR_NOTINITED; + // + pfout = f; + ooffset=0; + return ZR_OK; +} + +unsigned TZip::sflush(void *param,const char *buf, unsigned *size) +{ + // static + if (*size==0) + return 0; + TZip *zip = (TZip*)param; + unsigned int writ = zip->write(buf,*size); + if (writ!=0) + *size=0; + return writ; +} +unsigned TZip::swrite(void *param,const char *buf, unsigned size) +{ + // static + if (size==0) + return 0; + TZip *zip=(TZip*)param; + return zip->write(buf,size); +} +unsigned int TZip::write(const char *buf,unsigned int size) +{ + const char *srcbuf=buf; + if (pfout != NULL) + { + unsigned long writ = pfout->Write( srcbuf, size ); + return writ; + } + oerr=ZR_NOTINITED; + return 0; +} + +bool TZip::oseek(unsigned int pos) +{ + oerr=ZR_SEEK; + return false; +} + +ZRESULT TZip::Close() +{ + // if the directory hadn't already been added through a call to GetMemory, + // then we do it now + ZRESULT res=ZR_OK; + if (!hasputcen) + res=AddCentral(); + hasputcen=true; + pfout->Close(); + return res; +} + + + +#define ZIP_ATTR_READONLY 0x01 +#define ZIP_ATTR_HIDDEN 0x02 +#define ZIP_ATTR_SYSTEM 0x04 +#define ZIP_ATTR_DIRECTORY 0x10 +#define ZIP_ATTR_ARCHIVE 0x20 +#define ZIP_ATTR_DIRECTORY2 0x40000000 +#define ZIP_ATTR_NORMAL_FILE 0x80000000 +#define ZIP_ATTR_READABLE 0x01000000 +#define ZIP_ATTR_WRITEABLE 0x00800000 +#define ZIP_ATTR_EXECUTABLE 0x00400000 + +ZRESULT TZip::open_file(const TCHAR *fn) +{ + hfin=0; bufin=0; crc=CRCVAL_INITIAL; isize=0; csize=0; ired=0; + if (fn==0) + return ZR_ARGS; + hfin = new RageFile(); + if( !hfin->Open(fn) ) + { + SAFE_DELETE( hfin ); + return ZR_NOFILE; + } + isize = hfin->GetFileSize(); + attr= ZIP_ATTR_NORMAL_FILE | ZIP_ATTR_READABLE | ZIP_ATTR_WRITEABLE; + return set_times(); +} + +ZRESULT TZip::open_dir() +{ + hfin=0; bufin=0; crc=CRCVAL_INITIAL; isize=0; csize=0; ired=0; + attr= ZIP_ATTR_DIRECTORY2 | ZIP_ATTR_READABLE | ZIP_ATTR_WRITEABLE | ZIP_ATTR_DIRECTORY; + isize = 0; + return set_times(); +} + + +void filetime2dosdatetime(const tm st, unsigned short *dosdate,unsigned short *dostime) +{ + // date: bits 0-4 are day of month 1-31. Bits 5-8 are month 1..12. Bits 9-15 are year-1980 + // time: bits 0-4 are seconds/2, bits 5-10 are minute 0..59. Bits 11-15 are hour 0..23 + *dosdate = (unsigned short)(((st.tm_year+1900-1980)&0x7f) << 9); + *dosdate |= (unsigned short)(((st.tm_mon+1)&0xf) << 5); + *dosdate |= (unsigned short)((st.tm_mday&0x1f)); + *dostime = (unsigned short)((st.tm_hour&0x1f) << 11); + *dostime |= (unsigned short)((st.tm_min&0x3f) << 5); + *dostime |= (unsigned short)((st.tm_sec*2)&0x1f); +} + +ZRESULT TZip::set_times() +{ + time_t rawtime; + tm *ptm; + time ( &rawtime ); + ptm = localtime ( &rawtime ); + + unsigned short dosdate,dostime; + filetime2dosdatetime(*ptm,&dosdate,&dostime); + times.atime = time(NULL); + times.mtime = times.atime; + times.ctime = times.atime; + timestamp = (unsigned short)dostime | (((unsigned long)dosdate)<<16); + return ZR_OK; +} + +unsigned TZip::read(char *buf, unsigned size) +{ + if (bufin!=0) + { + if (posin>=lenin) return 0; // end of input + ulg red = lenin-posin; + if (red>size) + red=size; + memcpy(buf, bufin+posin, red); + posin += red; + ired += red; + crc = crc32(crc, (uch*)buf, red); + return red; + } + else if (hfin!=0) + { + int red = hfin->Read(buf,size); + if (red <= 0) + return 0; + ired += red; + crc = crc32(crc, (uch*)buf, red); + return red; + } + else + { + oerr=ZR_NOTINITED; + return 0; + } +} + +ZRESULT TZip::iclose() +{ + if (hfin!=0) + SAFE_DELETE( hfin); + bool mismatch = (isize!=-1 && isize!=ired); + isize=ired; // and crc has been being updated anyway + if (mismatch) + return ZR_MISSIZE; + else + return ZR_OK; +} + + +ZRESULT TZip::istore() +{ + ulg size=0; + for (;;) + { + unsigned int cin=read(buf,16384); + if (cin<=0 || cin==(unsigned int)EOF) + break; + unsigned int cout = write(buf,cin); + if (cout!=cin) + return ZR_MISSIZE; + size += cin; + } + csize=size; + return ZR_OK; +} + + + + + +bool has_seeded=false; +ZRESULT TZip::Add(const TCHAR *odstzn, const TCHAR *src,unsigned long flags) +{ + if (oerr) + return ZR_FAILED; + if (hasputcen) + return ZR_ENDED; + + // zip has its own notion of what its names should look like: i.e. dir/file.stuff + TCHAR dstzn[MAX_PATH]; _tcscpy(dstzn,odstzn); + if (*dstzn==0) + return ZR_ARGS; + TCHAR *d=dstzn; + while (*d!=0) + { + if (*d=='\\') + *d='/'; + d++; + } + bool isdir = (flags==ZIP_FOLDER); + bool needs_trailing_slash = (isdir && dstzn[_tcslen(dstzn)-1]!='/'); + int method=STORE; + + // now open whatever was our input source: + ZRESULT openres; + if (flags==ZIP_FILENAME) + openres=open_file(src); + else if (flags==ZIP_FOLDER) + openres=open_dir(); + else + return ZR_ARGS; + if (openres!=ZR_OK) + return openres; + + // A zip "entry" consists of a local header (which includes the file name), + // then the compressed data, and possibly an extended local header. + + // Initialize the local header + TZipFileInfo zfi; zfi.nxt=NULL; + strcpy(zfi.name,""); +#ifdef UNICODE + WideCharToMultiByte(CP_UTF8,0,dstzn,-1,zfi.iname,MAX_PATH,0,0); +#else + strcpy(zfi.iname,dstzn); +#endif + zfi.nam=strlen(zfi.iname); + if (needs_trailing_slash) + { + strcat(zfi.iname,"/"); + zfi.nam++; + } + strcpy(zfi.zname,""); + zfi.extra=NULL; zfi.ext=0; // extra header to go after this compressed data, and its length + zfi.cextra=NULL; zfi.cext=0; // extra header to go in the central end-of-zip directory, and its length + zfi.comment=NULL; zfi.com=0; // comment, and its length + zfi.mark = 1; + zfi.dosflag = 0; + zfi.att = (ush)BINARY; + zfi.vem = (ush)0xB17; // 0xB00 is win32 os-code. 0x17 is 23 in decimal: zip 2.3 + zfi.ver = (ush)20; // Needs PKUNZIP 2.0 to unzip it + zfi.tim = timestamp; + // Even though we write the header now, it will have to be rewritten, since we don't know compressed size or crc. + zfi.crc = 0; // to be updated later + zfi.flg = 8; // 8 means 'there is an extra header'. Assume for the moment that we need it. + zfi.lflg = zfi.flg; // to be updated later + zfi.how = (ush)method; // to be updated later + zfi.siz = (ulg)(method==STORE && isize>=0 ? isize : 0); // to be updated later + zfi.len = (ulg)(isize); // to be updated later + zfi.dsk = 0; + zfi.atx = attr; + zfi.off = writ+ooffset; // offset within file of the start of this local record + // stuff the 'times' structure into zfi.extra + + // nb. apparently there's a problem with PocketPC CE(zip)->CE(unzip) fails. And removing the following block fixes it up. + char xloc[EB_L_UT_SIZE]; zfi.extra=xloc; zfi.ext=EB_L_UT_SIZE; + char xcen[EB_C_UT_SIZE]; zfi.cextra=xcen; zfi.cext=EB_C_UT_SIZE; + xloc[0] = 'U'; + xloc[1] = 'T'; + xloc[2] = EB_UT_LEN(3); // length of data part of e.f. + xloc[3] = 0; + xloc[4] = EB_UT_FL_MTIME | EB_UT_FL_ATIME | EB_UT_FL_CTIME; + xloc[5] = (char)(times.mtime); + xloc[6] = (char)(times.mtime >> 8); + xloc[7] = (char)(times.mtime >> 16); + xloc[8] = (char)(times.mtime >> 24); + xloc[9] = (char)(times.atime); + xloc[10] = (char)(times.atime >> 8); + xloc[11] = (char)(times.atime >> 16); + xloc[12] = (char)(times.atime >> 24); + xloc[13] = (char)(times.ctime); + xloc[14] = (char)(times.ctime >> 8); + xloc[15] = (char)(times.ctime >> 16); + xloc[16] = (char)(times.ctime >> 24); + memcpy(zfi.cextra,zfi.extra,EB_C_UT_SIZE); + zfi.cextra[EB_LEN] = EB_UT_LEN(1); + + + // (1) Start by writing the local header: + int r = putlocal(&zfi,swrite,this); + if (r!=ZE_OK) + { + iclose(); + return ZR_WRITE; + } + writ += 4 + LOCHEAD + (unsigned int)zfi.nam + (unsigned int)zfi.ext; + if (oerr!=ZR_OK) + { + iclose(); + return oerr; + } + + //(2) Write stored file to zip file + ZRESULT writeres=ZR_OK; + if (!isdir && method==STORE) + writeres=istore(); + else if (isdir) + csize=0; + else + FAIL_M("deflate removed"); + iclose(); + writ += csize; + if (oerr!=ZR_OK) + return oerr; + if (writeres!=ZR_OK) + return ZR_WRITE; + + // (3) Either rewrite the local header with correct information... + bool first_header_has_size_right = (zfi.siz==csize); + zfi.crc = crc; + zfi.siz = csize; + zfi.len = isize; + // (4) ... or put an updated header at the end + if (zfi.how != (ush) method) + return ZR_NOCHANGE; + if (method==STORE && !first_header_has_size_right) + return ZR_NOCHANGE; + if ((r = putextended(&zfi, swrite,this)) != ZE_OK) + return ZR_WRITE; + writ += 16L; + zfi.flg = zfi.lflg; // if flg modified by inflate, for the central index + + if (oerr!=ZR_OK) + return oerr; + + // Keep a copy of the zipfileinfo, for our end-of-zip directory + char *cextra = new char[zfi.cext]; memcpy(cextra,zfi.cextra,zfi.cext); zfi.cextra=cextra; + TZipFileInfo *pzfi = new TZipFileInfo; memcpy(pzfi,&zfi,sizeof(zfi)); + if (zfis==NULL) + zfis=pzfi; + else + { + TZipFileInfo *z=zfis; + while (z->nxt!=NULL) + z=z->nxt; + z->nxt=pzfi; + } + return ZR_OK; +} + +ZRESULT TZip::AddCentral() +{ + // write central directory + int numentries = 0; + ulg pos_at_start_of_central = writ; + //ulg tot_unc_size=0, tot_compressed_size=0; + bool okay=true; + for (TZipFileInfo *zfi=zfis; zfi!=NULL; ) + { + if (okay) + { + int res = putcentral(zfi, swrite,this); + if (res!=ZE_OK) + okay=false; + } + writ += 4 + CENHEAD + (unsigned int)zfi->nam + (unsigned int)zfi->cext + (unsigned int)zfi->com; + //tot_unc_size += zfi->len; + //tot_compressed_size += zfi->siz; + numentries++; + // + TZipFileInfo *zfinext = zfi->nxt; + if (zfi->cextra!=0) delete[] zfi->cextra; + delete zfi; + zfi = zfinext; + } + ulg center_size = writ - pos_at_start_of_central; + if (okay) + { + int res = putend(numentries, center_size, pos_at_start_of_central+ooffset, 0, NULL, swrite,this); + if (res!=ZE_OK) + okay=false; + writ += 4 + ENDHEAD + 0; + } + if (!okay) + return ZR_WRITE; + return ZR_OK; +} + + + + + +ZRESULT lasterrorZ=ZR_OK; + + +CreateZip::CreateZip() +{ + hz=NULL; +} + +bool CreateZip::Start( RageFile *f) +{ + hz = new TZip(); + lasterrorZ = hz->Start(f); + return lasterrorZ == ZR_OK; +} +RString MakeDestZipFileName( RString fn ) +{ + // strip leading slash + fn.erase( fn.begin(), fn.begin()+1 ); + return fn; +} +bool CreateZip::AddFile(RString fn) +{ + lasterrorZ = hz->Add(MakeDestZipFileName(fn),fn,ZIP_FILENAME); + return lasterrorZ == ZR_OK; +} +bool CreateZip::AddDir(RString fn) +{ + lasterrorZ = hz->Add(MakeDestZipFileName(fn),NULL,ZIP_FOLDER); + return lasterrorZ == ZR_OK; +} +bool CreateZip::Finish() +{ + lasterrorZ = hz->Close(); + return lasterrorZ == ZR_OK; +} + +RString CreateZip::GetError() +{ + return FormatZipMessageZ( lasterrorZ ); +} + diff --git a/src/CreateZip.h b/src/CreateZip.h new file mode 100644 index 0000000000..64333ee7b0 --- /dev/null +++ b/src/CreateZip.h @@ -0,0 +1,93 @@ +// Adapted from http://www.codeproject.com/KB/files/zip_utils.aspx + +#ifndef CreateZip_H +#define CreateZip_H + + +// ZIP functions -- for creating zip files +// This file is a repackaged form of the Info-Zip source code available +// at www.info-zip.org. The original copyright notice may be found in +// zip.cpp. The repackaging was done by Lucian Wischik to simplify and +// extend its use in Windows/C++. Also to add encryption and unicode. + + +class TZip; +class RageFile; + +typedef unsigned long ZRESULT; +// return codes from any of the zip functions. Listed later. + + +class CreateZip +{ + TZip* hz; +public: + CreateZip(); + bool Start(RageFile *f); + bool AddFile(RString fn); + bool AddDir(RString fn); + bool Finish(); + RString GetError(); +}; + + + + +// e.g. +// +// (1) Traditional use, creating a zipfile from existing files +// HZIP hz = CreateZip("c:\\simple1.zip",0); +// ZipAdd(hz,"znsimple.bmp", "c:\\simple.bmp"); +// ZipAdd(hz,"znsimple.txt", "c:\\simple.txt"); +// CloseZip(hz); +// +// (2) Memory use, creating an auto-allocated mem-based zip file from various sources +// HZIP hz = CreateZip(0,100000, 0); +// // adding a conventional file... +// ZipAdd(hz,"src1.txt", "c:\\src1.txt"); +// // adding something from memory... +// char buf[1000]; for (int i=0; i<1000; i++) buf[i]=(char)(i&0x7F); +// ZipAdd(hz,"file.dat", buf,1000); +// // adding something from a pipe... +// HANDLE hread,hwrite; CreatePipe(&hread,&hwrite,NULL,0); +// HANDLE hthread = CreateThread(0,0,ThreadFunc,(void*)hwrite,0,0); +// ZipAdd(hz,"unz3.dat", hread,1000); // the '1000' is optional. +// WaitForSingleObject(hthread,INFINITE); +// CloseHandle(hthread); CloseHandle(hread); +// ... meanwhile DWORD WINAPI ThreadFunc(void *dat) +// { HANDLE hwrite = (HANDLE)dat; +// char buf[1000]={17}; +// DWORD writ; WriteFile(hwrite,buf,1000,&writ,NULL); +// CloseHandle(hwrite); +// return 0; +// } +// // and now that the zip is created, let's do something with it: +// void *zbuf; unsigned long zlen; ZipGetMemory(hz,&zbuf,&zlen); +// HANDLE hfz = CreateFile("test2.zip",GENERIC_WRITE,0,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0); +// DWORD writ; WriteFile(hfz,zbuf,zlen,&writ,NULL); +// CloseHandle(hfz); +// CloseZip(hz); +// +// (3) Handle use, for file handles and pipes +// HANDLE hzread,hzwrite; CreatePipe(&hzread,&hzwrite,0,0); +// HANDLE hthread = CreateThread(0,0,ZipReceiverThread,(void*)hzread,0,0); +// HZIP hz = CreateZipHandle(hzwrite,0); +// // ... add to it +// CloseZip(hz); +// CloseHandle(hzwrite); +// WaitForSingleObject(hthread,INFINITE); +// CloseHandle(hthread); +// ... meanwhile DWORD WINAPI ZipReceiverThread(void *dat) +// { HANDLE hread = (HANDLE)dat; +// char buf[1000]; +// while (true) +// { DWORD red; ReadFile(hread,buf,1000,&red,NULL); +// // ... and do something with this zip data we're receiving +// if (red==0) break; +// } +// CloseHandle(hread); +// return 0; +// } + + +#endif diff --git a/src/Makefile.am b/src/Makefile.am index a6f957a9d2..4e9f54d274 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -136,6 +136,7 @@ Course.cpp Course.h \ CourseLoaderCRS.cpp CourseLoaderCRS.h \ CourseUtil.cpp CourseUtil.h \ CourseWriterCRS.cpp CourseWriterCRS.h \ +CreateZip.cpp CreateZip.h \ CryptHelpers.cpp CryptHelpers.h \ DateTime.cpp DateTime.h \ Difficulty.cpp Difficulty.h \ diff --git a/src/NotesWriterJson.h b/src/NotesWriterJson.h index c873733f96..61a84e9b67 100644 --- a/src/NotesWriterJson.h +++ b/src/NotesWriterJson.h @@ -1,40 +1,40 @@ -/* NotesWriterJson - Writes a Song to a .json file. */ - -#ifndef NotesWriterJson_H -#define NotesWriterJson_H - -class Song; -class Steps; - -namespace NotesWriterJson -{ - static bool WriteSong( const RString &sFile, const Song &out, bool bWriteSteps ); - static bool WriteSteps( const RString &sFile, const Steps &out ); -}; - -#endif - -/* - * (c) 2001-2010 Chris Danford, Glenn Maynard - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +/* NotesWriterJson - Writes a Song to a .json file. */ + +#ifndef NotesWriterJson_H +#define NotesWriterJson_H + +class Song; +class Steps; + +namespace NotesWriterJson +{ + bool WriteSong( const RString &sFile, const Song &out, bool bWriteSteps ); + bool WriteSteps( const RString &sFile, const Steps &out ); +}; + +#endif + +/* + * (c) 2001-2010 Chris Danford, Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/RageUtil.cpp b/src/RageUtil.cpp index 43baf1756a..8b3273d575 100644 --- a/src/RageUtil.cpp +++ b/src/RageUtil.cpp @@ -1,2343 +1,2346 @@ -#include "global.h" -#include "RageUtil.h" -#include "RageMath.h" -#include "RageLog.h" -#include "RageFile.h" -#include "Foreach.h" -#include "LocalizedString.h" -#include "LuaBinding.h" -#include "LuaManager.h" -#include - -#include -#include -#include -#include -#include -#include -#include - -bool HexToBinary(const RString&, RString&); -void utf8_sanitize(RString &); -void UnicodeUpperLower(wchar_t *, size_t, const unsigned char *); - -RandomGen g_RandomNumberGenerator; - -MersenneTwister::MersenneTwister( int iSeed ) : m_iNext(0) -{ - Reset( iSeed ); -} - -void MersenneTwister::Reset( int iSeed ) -{ - if( iSeed == 0 ) - iSeed = time(NULL); - - m_Values[0] = iSeed; - m_iNext = 0; - for( int i = 1; i < 624; ++i ) - m_Values[i] = ((69069 * m_Values[i-1]) + 1) & 0xFFFFFFFF; - - GenerateValues(); -} - -void MersenneTwister::GenerateValues() -{ - static const int mask[] = { 0, 0x9908B0DF }; - - for( int i = 0; i < 227; ++i ) - { - int iVal = (m_Values[i] & 0x80000000) | (m_Values[i+1] & 0x7FFFFFFF); - int iNext = (i + 397); - - m_Values[i] = m_Values[iNext]; - m_Values[i] ^= (iVal >> 1); - m_Values[i] ^= mask[iVal&1]; - } - - for( int i = 227; i < 623; ++i ) - { - int iVal = (m_Values[i] & 0x80000000) | (m_Values[i+1] & 0x7FFFFFFF); - int iNext = (i + 397) - 624; - - m_Values[i] = m_Values[iNext]; - m_Values[i] ^= (iVal >> 1); - m_Values[i] ^= mask[iVal&1]; - } - - int iVal = (m_Values[623] & 0x80000000) + (m_Values[0] & 0x7FFFFFFF); - int iNext = (623 + 397) - 624; - m_Values[623] = m_Values[iNext] ^ (iVal>>1); - m_Values[623] ^= mask[iVal&1]; -} - -int MersenneTwister::Temper( int iVal ) -{ - iVal ^= (iVal >> 11); - iVal ^= (iVal << 7) & 0x9D2C5680; - iVal ^= (iVal << 15) & 0xEFC60000; - iVal ^= (iVal >> 18); - return iVal; -} - -int MersenneTwister::operator()() -{ - if( m_iNext == 624 ) - { - m_iNext = 0; - GenerateValues(); - } - - return Temper( m_Values[m_iNext++] ); -} - -/* Extend MersenneTwister into Lua space. This is intended to replace - * math.randomseed and math.random, so we conform to their behavior. */ - -namespace -{ - MersenneTwister g_LuaPRNG; - - /* To map from [0..2^32-1] to [0..1), we divide by 2^32. */ - const double DIVISOR = pow( double(2), double(32) ); - - static int Seed( lua_State *L ) - { - g_LuaPRNG.Reset( IArg(1) ); - return 0; - } - - static int Random( lua_State *L ) - { - switch( lua_gettop(L) ) - { - /* [0..1) */ - case 0: - { - double r = double(g_LuaPRNG()) / DIVISOR; - lua_pushnumber( L, r ); - return 1; - } - - /* [1..u] */ - case 1: - { - int upper = IArg(1); - luaL_argcheck( L, 1 <= upper, 1, "interval is empty" ); - lua_pushnumber( L, g_LuaPRNG(upper) + 1 ); - return 1; - } - /* [l..u] */ - case 2: - { - int lower = IArg(1); - int upper = IArg(2); - luaL_argcheck( L, lower < upper, 2, "interval is empty" ); - lua_pushnumber( L, (int(g_LuaPRNG()) % (upper-lower+1)) + lower ); - return 1; - } - - /* wrong amount of arguments */ - default: - { - return luaL_error( L, "wrong number of arguments" ); - } - } - } - - const luaL_Reg MersenneTwisterTable[] = - { - LIST_METHOD( Seed ), - LIST_METHOD( Random ), - { NULL, NULL } - }; -} - -LUA_REGISTER_NAMESPACE( MersenneTwister ); - -void fapproach( float& val, float other_val, float to_move ) -{ - ASSERT_M( to_move >= 0, ssprintf("to_move: %f < 0", to_move) ); - if( val == other_val ) - return; - float fDelta = other_val - val; - float fSign = fDelta / fabsf( fDelta ); - float fToMove = fSign*to_move; - if( fabsf(fToMove) > fabsf(fDelta) ) - fToMove = fDelta; // snap - val += fToMove; -} - -/* Return a positive x mod y. */ -float fmodfp(float x, float y) -{ - x = fmodf(x, y); /* x is [-y,y] */ - x += y; /* x is [0,y*2] */ - x = fmodf(x, y); /* x is [0,y] */ - return x; -} - -int power_of_two( int iInput ) -{ - int iValue = 1; - - while( iValue < iInput ) - iValue <<= 1; - - return iValue; -} - -bool IsAnInt( const RString &s ) -{ - if( !s.size() ) - return false; - - for( size_t i=0; i < s.size(); ++i ) - if( s[i] < '0' || s[i] > '9' ) - return false; - - return true; -} - -bool IsHexVal( const RString &s ) -{ - if( !s.size() ) - return false; - - for( size_t i=0; i < s.size(); ++i ) - if( !(s[i] >= '0' && s[i] <= '9') && - !(toupper(s[i]) >= 'A' && toupper(s[i]) <= 'F')) - return false; - - return true; -} - -RString BinaryToHex( const void *pData_, int iNumBytes ) -{ - const unsigned char *pData = (const unsigned char *) pData_; - RString s; - for( int i=0; i arrayBits; - split( sHHMMSS, ":", arrayBits, false ); - - while( arrayBits.size() < 3 ) - arrayBits.insert(arrayBits.begin(), "0" ); // pad missing bits - - float fSeconds = 0; - fSeconds += StringToInt( arrayBits[0] ) * 60 * 60; - fSeconds += StringToInt( arrayBits[1] ) * 60; - fSeconds += StringToFloat( arrayBits[2] ); - - return fSeconds; -} - -RString SecondsToHHMMSS( float fSecs ) -{ - const int iMinsDisplay = (int)fSecs/60; - const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; - RString sReturn = ssprintf( "%02d:%02d:%02d", iMinsDisplay/60, iMinsDisplay%60, iSecsDisplay ); - return sReturn; -} - -RString SecondsToMMSSMsMs( float fSecs ) -{ - const int iMinsDisplay = (int)fSecs/60; - const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; - const int iLeftoverDisplay = (int) ( (fSecs - iMinsDisplay*60 - iSecsDisplay) * 100 ); - RString sReturn = ssprintf( "%02d:%02d.%02d", iMinsDisplay, iSecsDisplay, min(99,iLeftoverDisplay) ); - return sReturn; -} - -RString SecondsToMSSMsMs( float fSecs ) -{ - const int iMinsDisplay = (int)fSecs/60; - const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; - const int iLeftoverDisplay = (int) ( (fSecs - iMinsDisplay*60 - iSecsDisplay) * 100 ); - RString sReturn = ssprintf( "%01d:%02d.%02d", iMinsDisplay, iSecsDisplay, min(99,iLeftoverDisplay) ); - return sReturn; -} - -RString SecondsToMMSSMsMsMs( float fSecs ) -{ - const int iMinsDisplay = (int)fSecs/60; - const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; - const int iLeftoverDisplay = (int) ( (fSecs - iMinsDisplay*60 - iSecsDisplay) * 1000 ); - RString sReturn = ssprintf( "%02d:%02d.%03d", iMinsDisplay, iSecsDisplay, min(999,iLeftoverDisplay) ); - return sReturn; -} - -RString SecondsToMSS( float fSecs ) -{ - const int iMinsDisplay = (int)fSecs/60; - const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; - RString sReturn = ssprintf( "%01d:%02d", iMinsDisplay, iSecsDisplay); - return sReturn; -} - -RString SecondsToMMSS( float fSecs ) -{ - const int iMinsDisplay = (int)fSecs/60; - const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; - RString sReturn = ssprintf( "%02d:%02d", iMinsDisplay, iSecsDisplay); - return sReturn; -} - -RString PrettyPercent( float fNumerator, float fDenominator) -{ - return ssprintf("%0.2f%%",fNumerator/fDenominator*100); -} - -RString Commify( int iNum ) -{ - RString sNum = ssprintf("%d",iNum); - return Commify( sNum ); -} - -RString Commify( RString sNum, RString sSeperator ) -{ - RString sReturn; - for( unsigned i=0; i= 3 && iPercent > 2 && sStr[iEnd-2] == 'l' && sStr[iEnd-1] == 'l' ) - { - sRet.append( sStr, iPercent, iEnd - iPercent - 2 ); // % - sRet.append( "I64" ); // %I64 - sRet.append( sStr, iEnd, 1 ); // %I64i - iOffset = iEnd + 1; - } - else - { - if( iEnd == sStr.npos ) - iEnd = sStr.size() - 1; - sRet.append( sStr, iOffset, iEnd - iOffset + 1 ); - iOffset = iEnd + 1; - } - } - return sRet; -} -#else -RString ConvertI64FormatString( const RString &sStr ) { return sStr; } -#endif - -/* ISO-639-1 codes: http://www.loc.gov/standards/iso639-2/php/code_list.php - * native forms: http://people.w3.org/rishida/names/languages.html - * We don't use 3-letter codes, so we don't bother supporting them. */ -static const LanguageInfo g_langs[] = -{ - {"aa", "Afar"}, - {"ab", "Abkhazian"}, - {"af", "Afrikaans"}, - {"am", "Amharic"}, - {"ar", "Arabic"}, - {"as", "Assamese"}, - {"ay", "Aymara"}, - {"az", "Azerbaijani"}, - {"ba", "Bashkir"}, - {"be", "Byelorussian"}, - {"bg", "Bulgarian"}, - {"bh", "Bihari"}, - {"bi", "Bislama"}, - {"bn", "Bengali"}, - {"bo", "Tibetan"}, - {"br", "Breton"}, - {"ca", "Catalan"}, - {"co", "Corsican"}, - {"cs", "Czech"}, - {"cy", "Welsh"}, - {"da", "Danish"}, - {"de", "German"}, - {"dz", "Bhutani"}, - {"el", "Greek"}, - {"en", "English"}, - {"eo", "Esperanto"}, - {"es", "Spanish"}, - {"et", "Estonian"}, - {"eu", "Basque"}, - {"fa", "Persian"}, - {"fi", "Finnish"}, - {"fj", "Fiji"}, - {"fo", "Faeroese"}, - {"fr", "French"}, - {"fy", "Frisian"}, - {"ga", "Irish"}, - {"gd", "Gaelic"}, - {"gl", "Galician"}, - {"gn", "Guarani"}, - {"gu", "Gujarati"}, - {"ha", "Hausa"}, - {"he", "Hebrew"}, - {"hi", "Hindi"}, - {"hr", "Croatian"}, - {"hu", "Hungarian"}, - {"hy", "Armenian"}, - {"ia", "Interlingua"}, - {"id", "Indonesian"}, - {"ie", "Interlingue"}, - {"ik", "Inupiak"}, - {"in", "Indonesian"}, // compatibility - {"is", "Icelandic"}, - {"it", "Italian"}, - {"iw", "Hebrew"}, // compatibility - {"ja", "Japanese"}, - {"ji", "Yiddish"}, // compatibility - {"jw", "Javanese"}, - {"ka", "Georgian"}, - {"kk", "Kazakh"}, - {"kl", "Greenlandic"}, - {"km", "Cambodian"}, - {"kn", "Kannada"}, - {"ko", "Korean"}, - {"ks", "Kashmiri"}, - {"ku", "Kurdish"}, - {"ky", "Kirghiz"}, - {"la", "Latin"}, - {"ln", "Lingala"}, - {"lo", "Laothian"}, - {"lt", "Lithuanian"}, - {"lv", "Latvian"}, - {"mg", "Malagasy"}, - {"mi", "Maori"}, - {"mk", "Macedonian"}, - {"ml", "Malayalam"}, - {"mn", "Mongolian"}, - {"mo", "Moldavian"}, - {"mr", "Marathi"}, - {"ms", "Malay"}, - {"mt", "Maltese"}, - {"my", "Burmese"}, - {"na", "Nauru"}, - {"ne", "Nepali"}, - {"nl", "Dutch"}, - {"no", "Norwegian"}, - {"oc", "Occitan"}, - {"om", "Oromo"}, - {"or", "Oriya"}, - {"pa", "Punjabi"}, - {"pl", "Polish"}, - {"ps", "Pashto"}, - {"pt", "Portuguese"}, - {"qu", "Quechua"}, - {"rm", "Rhaeto-Romance"}, - {"rn", "Kirundi"}, - {"ro", "Romanian"}, - {"ru", "Russian"}, - {"rw", "Kinyarwanda"}, - {"sa", "Sanskrit"}, - {"sd", "Sindhi"}, - {"sg", "Sangro"}, - {"sh", "Serbo-Croatian"}, - {"si", "Singhalese"}, - {"sk", "Slovak"}, - {"sl", "Slovenian"}, - {"sm", "Samoan"}, - {"sn", "Shona"}, - {"so", "Somali"}, - {"sq", "Albanian"}, - {"sr", "Serbian"}, - {"ss", "Siswati"}, - {"st", "Sesotho"}, - {"su", "Sudanese"}, - {"sv", "Swedish"}, - {"sw", "Swahili"}, - {"ta", "Tamil"}, - {"te", "Tegulu"}, - {"tg", "Tajik"}, - {"th", "Thai"}, - {"ti", "Tigrinya"}, - {"tk", "Turkmen"}, - {"tl", "Tagalog"}, - {"tn", "Setswana"}, - {"to", "Tonga"}, - {"tr", "Turkish"}, - {"ts", "Tsonga"}, - {"tt", "Tatar"}, - {"tw", "Twi"}, - {"uk", "Ukrainian"}, - {"ur", "Urdu"}, - {"uz", "Uzbek"}, - {"vi", "Vietnamese"}, - {"vo", "Volapuk"}, - {"wo", "Wolof"}, - {"xh", "Xhosa"}, - {"yi", "Yiddish"}, - {"yo", "Yoruba"}, - {"zh", "Chinese"}, - {"zu", "Zulu"}, -}; - -void GetLanguageInfos( vector &vAddTo ) -{ - for( unsigned i=0; i &sSource) -{ - if( sSource.empty() ) - return RString(); - - RString sTmp; - - for( unsigned iNum = 0; iNum < sSource.size()-1; iNum++ ) - { - sTmp += sSource[iNum]; - sTmp += sDeliminator; - } - sTmp += sSource.back(); - return sTmp; -} - -RString join( const RString &sDelimitor, vector::const_iterator begin, vector::const_iterator end ) -{ - if( begin == end ) - return RString(); - - RString sRet; - while( begin != end ) - { - sRet += *begin; - ++begin; - if( begin != end ) - sRet += sDelimitor; - } - - return sRet; -} - -RString SmEscape( const RString &sUnescaped ) -{ - return SmEscape( sUnescaped.c_str(), sUnescaped.size() ); -} - -RString SmEscape( const char *cUnescaped, int len ) -{ - RString answer = ""; - for( int i = 0; i < len; ++i ) - { - // Other characters we could theoretically escape: - // NotesWriterSM.cpp used to claim ',' should be escaped, but there was no explanation why - // '#' is both a control character and a valid part of a parameter. The only way for there to be - // any confusion is in a misformatted .sm file, though, so it is unnecessary to escape it. - if( cUnescaped[i] == '/' && i + 1 < len && cUnescaped[i + 1] == '/' ) - { - answer += "\\/\\/"; - ++i; // increment here so we skip both //s - continue; - } - if( cUnescaped[i] == '\\' || cUnescaped[i] == ':' || cUnescaped[i] == ';' ) - answer += "\\"; - answer += cUnescaped[i]; - } - return answer; -} - -RString DwiEscape( const RString &sUnescaped ) -{ - return DwiEscape( sUnescaped.c_str(), sUnescaped.size() ); -} - -RString DwiEscape( const char *cUnescaped, int len ) -{ - RString answer = ""; - for( int i = 0; i < len; ++i ) - { - switch( cUnescaped[i] ) - { - // TODO: Which of these characters actually affect DWI? - case '\\': - case ':': - case ';': answer += '|'; break; - case '[': answer += '('; break; - case ']': answer += ')'; break; - default: answer += cUnescaped[i]; - } - } - return answer; -} - -template -static int DelimitorLength( const S &Delimitor ) -{ - return Delimitor.size(); -} - -static int DelimitorLength( char Delimitor ) -{ - return 1; -} - -static int DelimitorLength( wchar_t Delimitor ) -{ - return 1; -} - -template -void do_split( const S &Source, const C Delimitor, vector &AddIt, const bool bIgnoreEmpty ) -{ - /* Short-circuit if the source is empty; we want to return an empty vector if - * the string is empty, even if bIgnoreEmpty is true. */ - if( Source.empty() ) - return; - - size_t startpos = 0; - - do { - size_t pos; - pos = Source.find( Delimitor, startpos ); - if( pos == Source.npos ) - pos = Source.size(); - - if( pos-startpos > 0 || !bIgnoreEmpty ) - { - /* Optimization: if we're copying the whole string, avoid substr; this - * allows this copy to be refcounted, which is much faster. */ - if( startpos == 0 && pos-startpos == Source.size() ) - AddIt.push_back(Source); - else - { - const S AddRString = Source.substr(startpos, pos-startpos); - AddIt.push_back(AddRString); - } - } - - startpos = pos+DelimitorLength(Delimitor); - } while ( startpos <= Source.size() ); -} - -void split( const RString &sSource, const RString &sDelimitor, vector &asAddIt, const bool bIgnoreEmpty ) -{ - if( sDelimitor.size() == 1 ) - do_split( sSource, sDelimitor[0], asAddIt, bIgnoreEmpty ); - else - do_split( sSource, sDelimitor, asAddIt, bIgnoreEmpty ); -} - -void split( const wstring &sSource, const wstring &sDelimitor, vector &asAddIt, const bool bIgnoreEmpty ) -{ - if( sDelimitor.size() == 1 ) - do_split( sSource, sDelimitor[0], asAddIt, bIgnoreEmpty ); - else - do_split( sSource, sDelimitor, asAddIt, bIgnoreEmpty ); -} - -/* Use: - -RString str="a,b,c"; -int start = 0, size = -1; -while( 1 ) -{ - do_split( str, ",", start, size ); - if( start == str.size() ) - break; - str[start] = 'Q'; -} - -*/ - -template -void do_split( const S &Source, const S &Delimitor, int &begin, int &size, int len, const bool bIgnoreEmpty ) -{ - if( size != -1 ) - { - // Start points to the beginning of the last delimiter. Move it up. - begin += size+Delimitor.size(); - begin = min( begin, len ); - } - - size = 0; - - if( bIgnoreEmpty ) - { - // Skip delims. - while( begin + Delimitor.size() < Source.size() && - !Source.compare( begin, Delimitor.size(), Delimitor ) ) - ++begin; - } - - /* Where's the string function to find within a substring? - * C++ strings apparently are missing that ... */ - size_t pos; - if( Delimitor.size() == 1 ) - pos = Source.find( Delimitor[0], begin ); - else - pos = Source.find( Delimitor, begin ); - if( pos == Source.npos || (int) pos > len ) - pos = len; - size = pos - begin; -} - -void split( const RString &Source, const RString &Delimitor, int &begin, int &size, int len, const bool bIgnoreEmpty ) -{ - do_split( Source, Delimitor, begin, size, len, bIgnoreEmpty ); -} - -void split( const wstring &Source, const wstring &Delimitor, int &begin, int &size, int len, const bool bIgnoreEmpty ) -{ - do_split( Source, Delimitor, begin, size, len, bIgnoreEmpty ); -} - -void split( const RString &Source, const RString &Delimitor, int &begin, int &size, const bool bIgnoreEmpty ) -{ - do_split( Source, Delimitor, begin, size, Source.size(), bIgnoreEmpty ); -} - -void split( const wstring &Source, const wstring &Delimitor, int &begin, int &size, const bool bIgnoreEmpty ) -{ - do_split( Source, Delimitor, begin, size, Source.size(), bIgnoreEmpty ); -} - -/* - * foo\fum\ -> "foo\fum\", "", "" - * c:\foo\bar.txt -> "c:\foo\", "bar", ".txt" - * \\foo\fum -> "\\foo\", "fum", "" - */ -void splitpath( const RString &sPath, RString &sDir, RString &sFilename, RString &sExt ) -{ - sDir = sFilename = sExt = ""; - - vector asMatches; - - /* - * One level of escapes for the regex, one for C. Ew. - * This is really: - * ^(.*[\\/])?(.*)$ - */ - static Regex sep("^(.*[\\\\/])?(.*)$"); - bool bCheck = sep.Compare( sPath, asMatches ); - ASSERT( bCheck ); - - sDir = asMatches[0]; - const RString sBase = asMatches[1]; - - /* ^(.*)(\.[^\.]+)$ */ - static Regex SplitExt("^(.*)(\\.[^\\.]+)$"); - if( SplitExt.Compare(sBase, asMatches) ) - { - sFilename = asMatches[0]; - sExt = asMatches[1]; - } - else - { - sFilename = sBase; - } -} - -/* "foo.bar", "baz" -> "foo.baz" - * "foo", "baz" -> "foo.baz" - * "foo.bar", "" -> "foo" */ -RString SetExtension( const RString &sPath, const RString &sExt ) -{ - RString sDir, sFileName, sOldExt; - splitpath( sPath, sDir, sFileName, sOldExt ); - return sDir + sFileName + (sExt.size()? ".":"") + sExt; -} - -RString GetExtension( const RString &sPath ) -{ - size_t pos = sPath.rfind( '.' ); - if( pos == sPath.npos ) - return RString(); - - size_t slash = sPath.find( '/', pos ); - if( slash != sPath.npos ) - return RString(); /* rare: path/dir.ext/fn */ - - return sPath.substr( pos+1, sPath.size()-pos+1 ); -} - -RString GetFileNameWithoutExtension( const RString &sPath ) -{ - RString sThrowAway, sFName; - splitpath( sPath, sThrowAway, sFName, sThrowAway ); - return sFName; -} - -void MakeValidFilename( RString &sName ) -{ - wstring wsName = RStringToWstring( sName ); - wstring wsInvalid = L"/\\:*?\"<>|"; - for( unsigned i = 0; i < wsName.size(); ++i ) - { - wchar_t w = wsName[i]; - if( w >= 32 && - w < 126 && - wsInvalid.find_first_of(w) == wsInvalid.npos ) - continue; - - if( w == L'"' ) - { - wsName[i] = L'\''; - continue; - } - - /* We could replace with closest matches in ASCII: convert the character - * to UTF-8 NFD (decomposed) (maybe NFKD?), and see if the first - * character is ASCII. This is useless for non-Western languages, - * since we'll replace the whole filename. */ - wsName[i] = '_'; - } - - sName = WStringToRString( wsName ); -} - -int g_argc = 0; -char **g_argv = NULL; - -void SetCommandlineArguments( int argc, char **argv ) -{ - g_argc = argc; - g_argv = argv; -} - -void GetCommandLineArguments( int &argc, char **&argv ) -{ - argc = g_argc; - argv = g_argv; -} - -/* Search for the commandline argument given; eg. "test" searches for the - * option "--test". All commandline arguments are getopt_long style: --foo; - * short arguments (-x) are not supported. (These are not intended for - * common, general use, so having short options isn't currently needed.) - * If argument is non-NULL, accept an argument. */ -bool GetCommandlineArgument( const RString &option, RString *argument, int iIndex ) -{ - const RString optstr = "--" + option; - - for( int arg = 1; arg < g_argc; ++arg ) - { - const RString CurArgument = g_argv[arg]; - - const size_t i = CurArgument.find( "=" ); - RString CurOption = CurArgument.substr(0,i); - if( CurOption.CompareNoCase(optstr) ) - continue; // no match - - // Found it. - if( iIndex ) - { - --iIndex; - continue; - } - - if( argument ) - { - if( i != RString::npos ) - *argument = CurArgument.substr( i+1 ); - else - *argument = ""; - } - - return true; - } - - return false; -} - -RString GetCwd() -{ - char buf[PATH_MAX]; - bool ret = getcwd(buf, PATH_MAX) != NULL; - ASSERT(ret); - return buf; -} - -/* - * Calculate a standard CRC32. iCRC should be initialized to 0. - * References: - * http://www.theorem.com/java/CRC32.java, - * http://www.faqs.org/rfcs/rfc1952.html - */ -void CRC32( unsigned int &iCRC, const void *pVoidBuffer, size_t iSize ) -{ - static unsigned tab[256]; - static bool initted = false; - if( !initted ) - { - initted = true; - const unsigned POLY = 0xEDB88320; - - for( int i = 0; i < 256; ++i ) - { - tab[i] = i; - for( int j = 0; j < 8; ++j ) - { - if( tab[i] & 1 ) - tab[i] = (tab[i] >> 1) ^ POLY; - else - tab[i] >>= 1; - } - } - } - - iCRC ^= 0xFFFFFFFF; - - const char *pBuffer = (const char *) pVoidBuffer; - for( unsigned i = 0; i < iSize; ++i ) - iCRC = (iCRC >> 8) ^ tab[(iCRC ^ pBuffer[i]) & 0xFF]; - - iCRC ^= 0xFFFFFFFF; -} - -unsigned int GetHashForString( const RString &s ) -{ - unsigned crc = 0; - CRC32( crc, s.data(), s.size() ); - return crc; -} - -/* Return true if "dir" is empty or does not exist. */ -bool DirectoryIsEmpty( const RString &sDir ) -{ - if( sDir.empty() ) - return true; - if( !DoesFileExist(sDir) ) - return true; - - vector asFileNames; - GetDirListing( sDir, asFileNames ); - return asFileNames.empty(); -} - -bool CompareRStringsAsc( const RString &sStr1, const RString &sStr2 ) -{ - return sStr1.CompareNoCase( sStr2 ) < 0; -} - -bool CompareRStringsDesc( const RString &sStr1, const RString &sStr2 ) -{ - return sStr1.CompareNoCase( sStr2 ) > 0; -} - -void SortRStringArray( vector &arrayRStrings, const bool bSortAscending ) -{ - sort( arrayRStrings.begin(), arrayRStrings.end(), - bSortAscending?CompareRStringsAsc:CompareRStringsDesc ); -} - -float calc_mean( const float *pStart, const float *pEnd ) -{ - return accumulate( pStart, pEnd, 0.f ) / distance( pStart, pEnd ); -} - -float calc_stddev( const float *pStart, const float *pEnd, bool bSample ) -{ - /* Calculate the mean. */ - float fMean = calc_mean( pStart, pEnd ); - - /* Calculate stddev. */ - float fDev = 0.0f; - for( const float *i=pStart; i != pEnd; ++i ) - fDev += (*i - fMean) * (*i - fMean); - fDev /= distance( pStart, pEnd ) - (bSample ? 1 : 0); - fDev = sqrtf( fDev ); - - return fDev; -} - -bool CalcLeastSquares( const vector< pair > &vCoordinates, - float &fSlope, float &fIntercept, float &fError ) -{ - if( vCoordinates.empty() ) - return false; - float fSumXX = 0.0f, fSumXY = 0.0f, fSumX = 0.0f, fSumY = 0.0f; - for( unsigned i = 0; i < vCoordinates.size(); ++i ) - { - fSumXX += vCoordinates[i].first * vCoordinates[i].first; - fSumXY += vCoordinates[i].first * vCoordinates[i].second; - fSumX += vCoordinates[i].first; - fSumY += vCoordinates[i].second; - } - const float fDenominator = vCoordinates.size() * fSumXX - fSumX * fSumX; - fSlope = (vCoordinates.size() * fSumXY - fSumX * fSumY) / fDenominator; - fIntercept = (fSumXX * fSumY - fSumX * fSumXY) / fDenominator; - - fError = 0.0f; - for( unsigned i = 0; i < vCoordinates.size(); ++i ) - { - const float fOneError = fIntercept + fSlope * vCoordinates[i].first - vCoordinates[i].second; - fError += fOneError * fOneError; - } - fError /= vCoordinates.size(); - fError = sqrtf( fError ); - return true; -} - -void FilterHighErrorPoints( vector< pair > &vCoordinates, - float fSlope, float fIntercept, float fCutoff ) -{ - unsigned int iOut = 0; - for( unsigned int iIn = 0; iIn < vCoordinates.size(); ++iIn ) - { - const float fError = fIntercept + fSlope * vCoordinates[iIn].first - vCoordinates[iIn].second; - if( fabsf(fError) < fCutoff ) - { - vCoordinates[iOut] = vCoordinates[iIn]; - ++iOut; - } - } - vCoordinates.resize( iOut ); -} - -void TrimLeft( RString &sStr, const char *s ) -{ - int n = 0; - while( n < int(sStr.size()) && strchr(s, sStr[n]) ) - n++; - - sStr.erase( sStr.begin(), sStr.begin()+n ); -} - -void TrimRight( RString &sStr, const char *s ) -{ - int n = sStr.size(); - while( n > 0 && strchr(s, sStr[n-1]) ) - n--; - - /* Delete from n to the end. If n == sStr.size(), nothing is deleted; - * if n == 0, the whole string is erased. */ - sStr.erase( sStr.begin()+n, sStr.end() ); -} - -void Trim( RString &sStr, const char *s ) -{ - RString::size_type b = 0, e = sStr.size(); - while( b < e && strchr(s, sStr[b]) ) - ++b; - while( b < e && strchr(s, sStr[e-1]) ) - --e; - sStr.assign( sStr.substr(b, e-b) ); -} - -void StripCrnl( RString &s ) -{ - while( s.size() && (s[s.size()-1] == '\r' || s[s.size()-1] == '\n') ) - s.erase( s.size()-1 ); -} - -bool BeginsWith( const RString &sTestThis, const RString &sBeginning ) -{ - ASSERT( !sBeginning.empty() ); - return sTestThis.compare( 0, sBeginning.length(), sBeginning ) == 0; -} - -bool EndsWith( const RString &sTestThis, const RString &sEnding ) -{ - ASSERT( !sEnding.empty() ); - if( sTestThis.size() < sEnding.size() ) - return false; - return sTestThis.compare( sTestThis.length()-sEnding.length(), sEnding.length(), sEnding ) == 0; -} - -RString URLEncode( const RString &sStr ) -{ - RString sOutput; - for( unsigned k = 0; k < sStr.size(); k++ ) - { - char t = sStr[k]; - if( t >= '!' && t <= 'z' ) - sOutput += t; - else - sOutput += "%" + ssprintf( "%02X", t ); - } - return sOutput; -} - -static bool CVSOrSVN( const RString& s ) -{ - return s.Right(3).EqualsNoCase("CVS") || s.Right(4) == ".svn"; -} - -void StripCvsAndSvn( vector &vs ) -{ - RemoveIf( vs, CVSOrSVN ); -} - -static bool MacResourceFork( const RString& s ) -{ - return s.Left(2).EqualsNoCase("._"); -} - -void StripMacResourceForks( vector &vs ) -{ - RemoveIf( vs, MacResourceFork ); -} - -// path is a .redir pathname. Read it and return the real one. -RString DerefRedir( const RString &_path ) -{ - RString sPath = _path; - - for( int i=0; i<100; i++ ) - { - if( GetExtension(sPath) != "redir" ) - return sPath; - - RString sNewFileName; - GetFileContents( sPath, sNewFileName, true ); - - // Empty is invalid. - if( sNewFileName == "" ) - return RString(); - - RString sPath2 = Dirname(sPath) + sNewFileName; - - CollapsePath( sPath2 ); - - sPath2 += "*"; - - vector matches; - GetDirListing( sPath2, matches, false, true ); - - if( matches.empty() ) - RageException::Throw( "The redirect \"%s\" references a file \"%s\" which doesn't exist.", sPath.c_str(), sPath2.c_str() ); - else if( matches.size() > 1 ) - RageException::Throw( "The redirect \"%s\" references a file \"%s\" with multiple matches.", sPath.c_str(), sPath2.c_str() ); - - sPath = matches[0]; - } - - RageException::Throw( "Circular redirect \"%s\".", sPath.c_str() ); -} - -bool GetFileContents( const RString &sPath, RString &sOut, bool bOneLine ) -{ - // Don't warn if the file doesn't exist, but do warn if it exists and fails to open. - if( !IsAFile(sPath) ) - return false; - - RageFile file; - if( !file.Open(sPath) ) - { - LOG->Warn( "GetFileContents(%s): %s", sPath.c_str(), file.GetError().c_str() ); - return false; - } - - // todo: figure out how to make this UTF-8 safe. -aj - RString sData; - int iGot; - if( bOneLine ) - iGot = file.GetLine( sData ); - else - iGot = file.Read( sData, file.GetFileSize() ); - - if( iGot == -1 ) - { - LOG->Warn( "GetFileContents(%s): %s", sPath.c_str(), file.GetError().c_str() ); - return false; - } - - if( bOneLine ) - StripCrnl( sData ); - - sOut = sData; - return true; -} - -bool GetFileContents( const RString &sFile, vector &asOut ) -{ - RageFile file; - if( !file.Open(sFile) ) - { - LOG->Warn( "GetFileContents(%s): %s", sFile.c_str(), file.GetError().c_str() ); - return false; - } - - RString sLine; - while( file.GetLine(sLine) ) - asOut.push_back( sLine ); - return true; -} - -#include "../extern/pcre/pcre.h" -void Regex::Compile() -{ - const char *error; - int offset; - m_pReg = pcre_compile( m_sPattern.c_str(), PCRE_CASELESS, &error, &offset, NULL ); - - if( m_pReg == NULL ) - RageException::Throw( "Invalid regex: \"%s\" (%s).", m_sPattern.c_str(), error ); - - int iRet = pcre_fullinfo( (pcre *) m_pReg, NULL, PCRE_INFO_CAPTURECOUNT, &m_iBackrefs ); - ASSERT( iRet >= 0 ); - - ++m_iBackrefs; - ASSERT( m_iBackrefs < 128 ); -} - -void Regex::Set( const RString &sStr ) -{ - Release(); - m_sPattern = sStr; - Compile(); -} - -void Regex::Release() -{ - pcre_free( m_pReg ); - m_pReg = NULL; - m_sPattern = RString(); -} - -Regex::Regex( const RString &sStr ): m_pReg(NULL), m_iBackrefs(0), m_sPattern(RString()) -{ - Set( sStr ); -} - -Regex::Regex( const Regex &rhs ): m_pReg(NULL), m_iBackrefs(0), m_sPattern(RString()) -{ - Set( rhs.m_sPattern ); -} - -Regex &Regex::operator=( const Regex &rhs ) -{ - if( this != &rhs ) - Set( rhs.m_sPattern ); - return *this; -} - -Regex::~Regex() -{ - Release(); -} - -bool Regex::Compare( const RString &sStr ) -{ - int iMat[128*3]; - int iRet = pcre_exec( (pcre *) m_pReg, NULL, sStr.data(), sStr.size(), 0, 0, iMat, 128*3 ); - - if( iRet < -1 ) - RageException::Throw( "Unexpected return from pcre_exec('%s'): %i.", m_sPattern.c_str(), iRet ); - - return iRet >= 0; -} - -bool Regex::Compare( const RString &sStr, vector &asMatches ) -{ - asMatches.clear(); - - int iMat[128*3]; - int iRet = pcre_exec( (pcre *) m_pReg, NULL, sStr.data(), sStr.size(), 0, 0, iMat, 128*3 ); - - if( iRet < -1 ) - RageException::Throw( "Unexpected return from pcre_exec('%s'): %i.", m_sPattern.c_str(), iRet ); - - if( iRet == -1 ) - return false; - - for( unsigned i = 1; i < m_iBackrefs; ++i ) - { - const int iStart = iMat[i*2], end = iMat[i*2+1]; - if( iStart == -1 ) - asMatches.push_back( RString() ); /* no match */ - else - asMatches.push_back( sStr.substr(iStart, end - iStart) ); - } - - return true; -} - -// Arguments and behavior are the same are similar to -// http://us3.php.net/manual/en/function.preg-replace.php -bool Regex::Replace( const RString &sReplacement, const RString &sSubject, RString &sOut ) -{ - vector asMatches; - if( !Compare(sSubject, asMatches) ) - return false; - - sOut = sReplacement; - - // TODO: optimize me by iterating only once over the string - for( unsigned i=0; i= s.size() ) - return false; - - if( is_utf8_continuation_byte( s[start] ) || /* misplaced continuation byte */ - (s[start] & 0xFE) == 0xFE ) /* 0xFE, 0xFF */ - { - start += 1; - return false; - } - - int len = utf8_get_char_len( s[start] ); - - const int first_byte_mask[] = { -1, 0x7F, 0x1F, 0x0F, 0x07, 0x03, 0x01 }; - - ch = wchar_t(s[start] & first_byte_mask[len]); - - for( int i = 1; i < len; ++i ) - { - if( start+i >= s.size() ) - { - /* We expected a continuation byte, but didn't get one. Return error, and point - * start at the unexpected byte; it's probably a new sequence. */ - start += i; - return false; - } - - char byte = s[start+i]; - if( !is_utf8_continuation_byte(byte) ) - { - /* We expected a continuation byte, but didn't get one. Return error, and point - * start at the unexpected byte; it's probably a new sequence. */ - start += i; - return false; - } - ch = (ch << 6) | (byte & 0x3F); - } - - bool bValid = true; - { - unsigned c1 = (unsigned) s[start] & 0xFF; - unsigned c2 = (unsigned) s[start+1] & 0xFF; - int c = (c1 << 8) + c2; - if( (c & 0xFE00) == 0xC000 || - (c & 0xFFE0) == 0xE080 || - (c & 0xFFF0) == 0xF080 || - (c & 0xFFF8) == 0xF880 || - (c & 0xFFFC) == 0xFC80 ) - { - bValid = false; - } - } - - if( ch == 0xFFFE || ch == 0xFFFF ) - bValid = false; - - start += len; - return bValid; -} - -/* Like utf8_to_wchar_ec, but only does enough error checking to prevent crashing. */ -bool utf8_to_wchar( const char *s, size_t iLength, unsigned &start, wchar_t &ch ) -{ - if( start >= iLength ) - return false; - - int len = utf8_get_char_len( s[start] ); - - if( start+len > iLength ) - { - // We don't have room for enough continuation bytes. Return error. - start += len; - ch = L'?'; - return false; - } - - switch( len ) - { - case 1: - ch = (s[start+0] & 0x7F); - break; - case 2: - ch = ( (s[start+0] & 0x1F) << 6 ) | - (s[start+1] & 0x3F); - break; - case 3: - ch = ( (s[start+0] & 0x0F) << 12 ) | - ( (s[start+1] & 0x3F) << 6 ) | - (s[start+2] & 0x3F); - break; - case 4: - ch = ( (s[start+0] & 0x07) << 18 ) | - ( (s[start+1] & 0x3F) << 12 ) | - ( (s[start+2] & 0x3F) << 6 ) | - (s[start+3] & 0x3F); - break; - case 5: - ch = ( (s[start+0] & 0x03) << 24 ) | - ( (s[start+1] & 0x3F) << 18 ) | - ( (s[start+2] & 0x3F) << 12 ) | - ( (s[start+3] & 0x3F) << 6 ) | - (s[start+4] & 0x3F); - break; - - case 6: - ch = ( (s[start+0] & 0x01) << 30 ) | - ( (s[start+1] & 0x3F) << 24 ) | - ( (s[start+2] & 0x3F) << 18 ) | - ( (s[start+3] & 0x3F) << 12) | - ( (s[start+4] & 0x3F) << 6 ) | - (s[start+5] & 0x3F); - break; - - } - - start += len; - return true; -} - - -// UTF-8 encode ch and append to out. -void wchar_to_utf8( wchar_t ch, RString &out ) -{ - if( ch < 0x80 ) { out.append( 1, (char) ch ); return; } - - int cbytes = 0; - if( ch < 0x800 ) cbytes = 1; - else if( ch < 0x10000 ) cbytes = 2; - else if( ch < 0x200000 ) cbytes = 3; - else if( ch < 0x4000000 ) cbytes = 4; - else cbytes = 5; - - { - int shift = cbytes*6; - const int init_masks[] = { 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; - out.append( 1, (char) (init_masks[cbytes-1] | (ch>>shift)) ); - } - - for( int i = 0; i < cbytes; ++i ) - { - int shift = (cbytes-i-1)*6; - out.append( 1, (char) (0x80 | ((ch>>shift)&0x3F)) ); - } -} - -wchar_t utf8_get_char( const RString &s ) -{ - unsigned start = 0; - wchar_t ret; - if( !utf8_to_wchar_ec( s, start, ret ) ) - return INVALID_CHAR; - return ret; -} - -// Replace invalid sequences in s. -void utf8_sanitize( RString &s ) -{ - RString ret; - for( unsigned start = 0; start < s.size(); ) - { - wchar_t ch; - if( !utf8_to_wchar_ec( s, start, ch ) ) - ch = INVALID_CHAR; - - wchar_to_utf8( ch, ret ); - } - - s = ret; -} - -bool utf8_is_valid( const RString &s ) -{ - for( unsigned start = 0; start < s.size(); ) - { - wchar_t ch; - if( !utf8_to_wchar_ec( s, start, ch ) ) - return false; - } - return true; -} - -/* Windows tends to drop garbage BOM characters at the start of UTF-8 text files. - * Remove them. */ -void utf8_remove_bom( RString &sLine ) -{ - if( !sLine.compare(0, 3, "\xef\xbb\xbf") ) - sLine.erase(0, 3); -} - -static int UnicodeDoUpper( char *p, size_t iLen, const unsigned char pMapping[256] ) -{ - // Note: this has problems with certain accented characters. -aj - wchar_t wc = L'\0'; - unsigned iStart = 0; - if( !utf8_to_wchar(p, iLen, iStart, wc) ) - return 1; - - wchar_t iUpper = wc; - if( wc < 256 ) - iUpper = pMapping[wc]; - if( iUpper != wc ) - { - RString sOut; - wchar_to_utf8( iUpper, sOut ); - if( sOut.size() == iStart ) - memcpy( p, sOut.data(), sOut.size() ); - else - WARN( ssprintf("UnicodeDoUpper: invalid character at \"%s\"", RString(p,iLen).c_str()) ); - } - - return iStart; -} - -/* Fast in-place MakeUpper and MakeLower. This only replaces characters with characters of the same UTF-8 - * length, so we never have to move the whole string. This is optimized for strings that have no - * non-ASCII characters. */ -void MakeUpper( char *p, size_t iLen ) -{ - char *pStart = p; - char *pEnd = p + iLen; - while( p < pEnd ) - { - // Fast path: - if( likely( !(*p & 0x80) ) ) - { - if( unlikely(*p >= 'a' && *p <= 'z') ) - *p += 'A' - 'a'; - ++p; - continue; - } - - int iRemaining = iLen - (p-pStart); - p += UnicodeDoUpper( p, iRemaining, g_UpperCase ); - } -} - -void MakeLower( char *p, size_t iLen ) -{ - char *pStart = p; - char *pEnd = p + iLen; - while( p < pEnd ) - { - // Fast path: - if( likely( !(*p & 0x80) ) ) - { - if( unlikely(*p >= 'A' && *p <= 'Z') ) - *p -= 'A' - 'a'; - ++p; - continue; - } - - int iRemaining = iLen - (p-pStart); - p += UnicodeDoUpper( p, iRemaining, g_LowerCase ); - } -} - -void UnicodeUpperLower( wchar_t *p, size_t iLen, const unsigned char pMapping[256] ) -{ - wchar_t *pEnd = p + iLen; - while( p != pEnd ) - { - if( *p < 256 ) - *p = pMapping[*p]; - ++p; - } -} - -void MakeUpper( wchar_t *p, size_t iLen ) -{ - UnicodeUpperLower( p, iLen, g_UpperCase ); -} - -void MakeLower( wchar_t *p, size_t iLen ) -{ - UnicodeUpperLower( p, iLen, g_LowerCase ); -} - -int StringToInt( const RString &sString ) -{ - int ret; - istringstream ( sString ) >> ret; - return ret; -} - -RString IntToString( const int &iNum ) -{ - stringstream ss; - ss << iNum; - return ss.str(); -} - -float StringToFloat( const RString &sString ) -{ - float ret = strtof( sString, NULL ); - - if( !isfinite(ret) ) - ret = 0.0f; - return ret; -} - -bool StringToFloat( const RString &sString, float &fOut ) -{ - char *endPtr; - - fOut = strtof( sString, &endPtr ); - return sString.size() && *endPtr == '\0' && isfinite( fOut ); -} - -RString FloatToString( const float &num ) -{ - stringstream ss; - ss << num; - return ss.str(); -} - -const wchar_t INVALID_CHAR = 0xFFFD; /* U+FFFD REPLACEMENT CHARACTER */ - -wstring RStringToWstring( const RString &s ) -{ - wstring ret; - ret.reserve( s.size() ); - for( unsigned start = 0; start < s.size(); ) - { - char c = s[start]; - if( !(c&0x80) ) - { - // ASCII fast path - ret += c; - ++start; - continue; - } - - wchar_t ch = L'\0'; - if( !utf8_to_wchar( s.data(), s.size(), start, ch ) ) - ch = INVALID_CHAR; - ret += ch; - } - - return ret; -} - -RString WStringToRString( const wstring &sStr ) -{ - RString sRet; - - for( unsigned i = 0; i < sStr.size(); ++i ) - wchar_to_utf8( sStr[i], sRet ); - - return sRet; -} - -RString WcharToUTF8( wchar_t c ) -{ - RString ret; - wchar_to_utf8( c, ret ); - return ret; -} - -// &a; -> a -void ReplaceEntityText( RString &sText, const map &m ) -{ - RString sRet; - - size_t iOffset = 0; - while( iOffset != sText.size() ) - { - size_t iStart = sText.find( '&', iOffset ); - if( iStart == sText.npos ) - { - // Optimization: if we didn't replace anything at all, do nothing. - if( iOffset == 0 ) - return; - - // Append the rest of the string. - sRet.append( sText, iOffset, sRet.npos ); - break; - } - - // Append the text between iOffset and iStart. - sRet.append( sText, iOffset, iStart-iOffset ); - iOffset += iStart-iOffset; - - // Optimization: stop early on "&", so "&&&&&&&&&&&" isn't n^2. - size_t iEnd = sText.find_first_of( "&;", iStart+1 ); - if( iEnd == sText.npos || sText[iEnd] == '&' ) - { - // & with no matching ;, or two & in a row. Append the & and continue. - sRet.append( sText, iStart, 1 ); - ++iOffset; - continue; - } - - RString sElement = sText.substr( iStart+1, iEnd-iStart-1 ); - sElement.MakeLower(); - - map::const_iterator it = m.find( sElement ); - if( it == m.end() ) - { - sRet.append( sText, iStart, iEnd-iStart+1 ); - iOffset = iEnd + 1; - continue; - } - - const RString &sTo = it->second; - sRet.append( sTo ); - iOffset = iEnd + 1; - } - - sText = sRet; -} - -// abcd -> &a; &b; &c; &d; -void ReplaceEntityText( RString &sText, const map &m ) -{ - RString sFind; - - FOREACHM_CONST( char, RString, m, c ) - sFind.append( 1, c->first ); - - RString sRet; - - size_t iOffset = 0; - while( iOffset != sText.size() ) - { - size_t iStart = sText.find_first_of( sFind, iOffset ); - if( iStart == sText.npos ) - { - // Optimization: if we didn't replace anything at all, do nothing. - if( iOffset == 0 ) - return; - - // Append the rest of the string. - sRet.append( sText, iOffset, sRet.npos ); - break; - } - - // Append the text between iOffset and iStart. - sRet.append( sText, iOffset, iStart-iOffset ); - iOffset += iStart-iOffset; - - char sElement = sText[iStart]; - - map::const_iterator it = m.find( sElement ); - ASSERT( it != m.end() ); - - const RString &sTo = it->second; - sRet.append( 1, '&' ); - sRet.append( sTo ); - sRet.append( 1, ';' ); - ++iOffset; - } - - sText = sRet; -} - -// Replace &#nnnn; (decimal) and &xnnnn; (hex) with corresponding UTF-8 characters. -void Replace_Unicode_Markers( RString &sText ) -{ - unsigned iStart = 0; - while( iStart < sText.size() ) - { - // Look for &#digits; - bool bHex = false; - size_t iPos = sText.find( "&#", iStart ); - if( iPos == sText.npos ) - { - bHex = true; - iPos = sText.find( "&x", iStart ); - } - - if( iPos == sText.npos ) - break; - iStart = iPos+1; - - unsigned p = iPos; - p += 2; - - // Found &# or &x. Is it followed by digits and a semicolon? - if( p >= sText.size() ) - continue; - - int iNumDigits = 0; - while( p < sText.size() && bHex? isxdigit(sText[p]):isdigit(sText[p]) ) - { - p++; - iNumDigits++; - } - - if( !iNumDigits ) - continue; // must have at least one digit - if( p >= sText.size() || sText[p] != ';' ) - continue; - p++; - - int iNum; - if( bHex ) - sscanf( sText.c_str()+iPos, "&x%x;", &iNum ); - else - sscanf( sText.c_str()+iPos, "&#%i;", &iNum ); - if( iNum > 0xFFFF ) - iNum = INVALID_CHAR; - - sText.replace( iPos, p-iPos, WcharToUTF8(wchar_t(iNum)) ); - } -} - -// Form a string to identify a wchar_t with ASCII. -RString WcharDisplayText( wchar_t c ) -{ - RString sChr; - sChr = ssprintf( "U+%4.4x", c ); - if( c < 128 ) - sChr += ssprintf( " ('%c')", char(c) ); - return sChr; -} - -/* Return the last named component of dir: - * a/b/c -> c - * a/b/c/ -> c - */ -RString Basename( const RString &sDir ) -{ - size_t iEnd = sDir.find_last_not_of( "/\\" ); - if( iEnd == sDir.npos ) - return RString(); - - size_t iStart = sDir.find_last_of( "/\\", iEnd ); - if( iStart == sDir.npos ) - iStart = 0; - else - ++iStart; - - return sDir.substr( iStart, iEnd-iStart+1 ); -} - -/* Return all but the last named component of dir: - * - * a/b/c -> a/b/ - * a/b/c/ -> a/b/ - * c/ -> ./ - * /foo -> / - * / -> / - */ -RString Dirname( const RString &dir ) -{ - // Special case: "/" -> "/". - if( dir.size() == 1 && dir[0] == '/' ) - return "/"; - - int pos = dir.size()-1; - // Skip trailing slashes. - while( pos >= 0 && dir[pos] == '/' ) - --pos; - - // Skip the last component. - while( pos >= 0 && dir[pos] != '/' ) - --pos; - - if( pos < 0 ) - return "./"; - - return dir.substr(0, pos+1); -} - -RString Capitalize( const RString &s ) -{ - if( s.empty() ) - return RString(); - - RString s2 = s; - char *pBuf = s2.GetBuffer(); - UnicodeDoUpper( pBuf, s2.size(), g_UpperCase ); - s2.ReleaseBuffer(); - - return s2; -} - -unsigned char g_UpperCase[256] = -{ - 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F, - 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, - 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F, - 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F, - 0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F, - 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F, - 0x60,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F, - 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x7B,0x7C,0x7D,0x7E,0x7F, - 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8A,0x8B,0x8C,0x8D,0x8E,0x8F, - 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C,0x9D,0x9E,0x9F, - 0xA0,0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xAB,0xAC,0xAD,0xAE,0xAF, - 0xB0,0xB1,0xB2,0xB3,0xB4,0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xBB,0xBC,0xBD,0xBE,0xBF, - 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, - 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xDF, - 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, - 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xF7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xFF, -}; - -unsigned char g_LowerCase[256] = -{ - 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F, - 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, - 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F, - 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F, - 0x40,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F, - 0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x5B,0x5C,0x5D,0x5E,0x5F, - 0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F, - 0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x7F, - 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8A,0x8B,0x8C,0x8D,0x8E,0x8F, - 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C,0x9D,0x9E,0x9F, - 0xA0,0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xAB,0xAC,0xAD,0xAE,0xAF, - 0xB0,0xB1,0xB2,0xB3,0xB4,0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xBB,0xBC,0xBD,0xBE,0xBF, - 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, - 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xDF, - 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, - 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xF7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xFF, -}; - -void FixSlashesInPlace( RString &sPath ) -{ - for( unsigned i = 0; i < sPath.size(); ++i ) - if( sPath[i] == '\\' ) - sPath[i] = '/'; -} - -/* Keep trailing slashes, since that can be used to illustrate that a path always - * represents a directory. - * - * foo/bar -> foo/bar - * foo/bar/ -> foo/bar/ - * foo///bar/// -> foo/bar/ - * foo/bar/./baz -> foo/bar/baz - * foo/bar/../baz -> foo/baz - * ../foo -> ../foo - * ../../foo -> ../../foo - * ./foo -> foo (if bRemoveLeadingDot), ./foo (if !bRemoveLeadingDot) - * ./ -> ./ - * ./// -> ./ - */ - -void CollapsePath( RString &sPath, bool bRemoveLeadingDot ) -{ - RString sOut; - sOut.reserve( sPath.size() ); - - size_t iPos = 0; - size_t iNext; - for( ; iPos < sPath.size(); iPos = iNext ) - { - // Find the next slash. - iNext = sPath.find( '/', iPos ); - if( iNext == RString::npos ) - iNext = sPath.size(); - else - ++iNext; - - /* Strip extra slashes, but don't remove slashes from the beginning of the string. */ - if( iNext - iPos == 1 && sPath[iPos] == '/' ) - { - if( !sOut.empty() ) - continue; - } - - // If this is a dot, skip it. - if( iNext - iPos == 2 && sPath[iPos] == '.' && sPath[iPos+1] == '/' ) - { - if( bRemoveLeadingDot || !sOut.empty() ) - continue; - } - - // If this is two dots, - if( iNext - iPos == 3 && sPath[iPos] == '.' && sPath[iPos+1] == '.' && sPath[iPos+2] == '/' ) - { - /* If this is the first path element (nothing to delete), - * or all we have is a slash, leave it. */ - if( sOut.empty() || (sOut.size() == 1 && sOut[0] == '/') ) - { - sOut.append( sPath, iPos, iNext-iPos ); - continue; - } - - // Search backwards for the previous path element. - size_t iPrev = sOut.rfind( '/', sOut.size()-2 ); - if( iPrev == RString::npos ) - iPrev = 0; - else - ++iPrev; - - // If the previous element is also .., leave it. - bool bLastIsTwoDots = (sOut.size() - iPrev == 3 && sOut[iPrev] == '.' && sOut[iPrev+1] == '.' ); - if( bLastIsTwoDots ) - { - sOut.append( sPath, iPos, iNext-iPos ); - continue; - } - - sOut.erase( iPrev ); - continue; - } - - sOut.append( sPath, iPos, iNext-iPos ); - } - - sOut.swap( sPath ); -} - -namespace StringConversion -{ - template<> bool FromString( const RString &sValue, int &out ) - { - if( sscanf( sValue.c_str(), "%d", &out ) == 1 ) - return true; - - out = 0; - return false; - } - - template<> bool FromString( const RString &sValue, unsigned &out ) - { - if( sscanf( sValue.c_str(), "%u", &out ) == 1 ) - return true; - - out = 0; - return false; - } - - template<> bool FromString( const RString &sValue, float &out ) - { - const char *endptr = sValue.data() + sValue.size(); - out = strtof( sValue, (char **) &endptr ); - if( endptr != sValue.data() && isfinite( out ) ) - return true; - out = 0; - return false; - } - - template<> bool FromString( const RString &sValue, bool &out ) - { - if( sValue.size() == 0 ) - return false; - - out = (StringToInt(sValue) != 0); - return true; - } - - template<> RString ToString( const int &value ) - { - return ssprintf( "%i", value ); - } - - template<> RString ToString( const unsigned &value ) - { - return ssprintf( "%u", value ); - } - - template<> RString ToString( const float &value ) - { - return ssprintf( "%f", value ); - } - - template<> RString ToString( const bool &value ) - { - return ssprintf( "%i", value ); - } -} - -bool FileCopy( const RString &sSrcFile, const RString &sDstFile ) -{ - if( !sSrcFile.CompareNoCase(sDstFile) ) - { - LOG->Warn( "Tried to copy \"%s\" over itself", sSrcFile.c_str() ); - return false; - } - - RageFile in; - if( !in.Open(sSrcFile, RageFile::READ) ) - return false; - - RageFile out; - if( !out.Open(sDstFile, RageFile::WRITE) ) - return false; - - RString sError; - if( !FileCopy(in, out, sError) ) - { - LOG->Warn( "FileCopy(%s,%s): %s", - sSrcFile.c_str(), sDstFile.c_str(), sError.c_str() ); - return false; - } - - return true; -} - -bool FileCopy( RageFileBasic &in, RageFileBasic &out, RString &sError, bool *bReadError ) -{ - while(1) - { - RString data; - if( in.Read(data, 1024*32) == -1 ) - { - sError = ssprintf( "read error: %s", in.GetError().c_str() ); - if( bReadError != NULL ) - *bReadError = true; - return false; - } - if( data.empty() ) - break; - int i = out.Write(data); - if( i == -1 ) - { - sError = ssprintf( "write error: %s", out.GetError().c_str() ); - if( bReadError != NULL ) - *bReadError = false; - return false; - } - } - - if( out.Flush() == -1 ) - { - sError = ssprintf( "write error: %s", out.GetError().c_str() ); - if( bReadError != NULL ) - *bReadError = false; - return false; - } - - return true; -} - -LuaFunction( SecondsToMSSMsMs, SecondsToMSSMsMs( FArg(1) ) ) -LuaFunction( SecondsToHHMMSS, SecondsToHHMMSS( FArg(1) ) ) -LuaFunction( SecondsToMMSSMsMs, SecondsToMMSSMsMs( FArg(1) ) ) -LuaFunction( SecondsToMMSSMsMsMs, SecondsToMMSSMsMsMs( FArg(1) ) ) -LuaFunction( SecondsToMSS, SecondsToMSS( FArg(1) ) ) -LuaFunction( SecondsToMMSS, SecondsToMMSS( FArg(1) ) ) -LuaFunction( FormatNumberAndSuffix, FormatNumberAndSuffix( IArg(1) ) ) -LuaFunction( Basename, Basename( SArg(1) ) ) -static RString MakeLower( RString s ) { s.MakeLower(); return s; } -LuaFunction( Lowercase, MakeLower( SArg(1) ) ) -static RString MakeUpper( RString s ) { s.MakeUpper(); return s; } -LuaFunction( Uppercase, MakeUpper( SArg(1) ) ) -LuaFunction( mbstrlen, (int)RStringToWstring(SArg(1)).length() ) -LuaFunction( URLEncode, URLEncode( SArg(1) ) ); -LuaFunction( PrettyPercent, PrettyPercent( FArg(1), FArg(2) ) ); -//LuaFunction( IsHexVal, IsHexVal( SArg(1) ) ); -static bool UndocumentedFeature( RString s ){ sm_crash(s); return true; } -LuaFunction( UndocumentedFeature, UndocumentedFeature(SArg(1)) ); - -/* - * Copyright (c) 2001-2005 Chris Danford, Glenn Maynard - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#include "global.h" +#include "RageUtil.h" +#include "RageMath.h" +#include "RageLog.h" +#include "RageFile.h" +#include "Foreach.h" +#include "LocalizedString.h" +#include "LuaBinding.h" +#include "LuaManager.h" +#include + +#include +#include +#include +#include +#include +#include +#include + +bool HexToBinary(const RString&, RString&); +void utf8_sanitize(RString &); +void UnicodeUpperLower(wchar_t *, size_t, const unsigned char *); + +RandomGen g_RandomNumberGenerator; + +MersenneTwister::MersenneTwister( int iSeed ) : m_iNext(0) +{ + Reset( iSeed ); +} + +void MersenneTwister::Reset( int iSeed ) +{ + if( iSeed == 0 ) + iSeed = time(NULL); + + m_Values[0] = iSeed; + m_iNext = 0; + for( int i = 1; i < 624; ++i ) + m_Values[i] = ((69069 * m_Values[i-1]) + 1) & 0xFFFFFFFF; + + GenerateValues(); +} + +void MersenneTwister::GenerateValues() +{ + static const int mask[] = { 0, 0x9908B0DF }; + + for( int i = 0; i < 227; ++i ) + { + int iVal = (m_Values[i] & 0x80000000) | (m_Values[i+1] & 0x7FFFFFFF); + int iNext = (i + 397); + + m_Values[i] = m_Values[iNext]; + m_Values[i] ^= (iVal >> 1); + m_Values[i] ^= mask[iVal&1]; + } + + for( int i = 227; i < 623; ++i ) + { + int iVal = (m_Values[i] & 0x80000000) | (m_Values[i+1] & 0x7FFFFFFF); + int iNext = (i + 397) - 624; + + m_Values[i] = m_Values[iNext]; + m_Values[i] ^= (iVal >> 1); + m_Values[i] ^= mask[iVal&1]; + } + + int iVal = (m_Values[623] & 0x80000000) + (m_Values[0] & 0x7FFFFFFF); + int iNext = (623 + 397) - 624; + m_Values[623] = m_Values[iNext] ^ (iVal>>1); + m_Values[623] ^= mask[iVal&1]; +} + +int MersenneTwister::Temper( int iVal ) +{ + iVal ^= (iVal >> 11); + iVal ^= (iVal << 7) & 0x9D2C5680; + iVal ^= (iVal << 15) & 0xEFC60000; + iVal ^= (iVal >> 18); + return iVal; +} + +int MersenneTwister::operator()() +{ + if( m_iNext == 624 ) + { + m_iNext = 0; + GenerateValues(); + } + + return Temper( m_Values[m_iNext++] ); +} + +/* Extend MersenneTwister into Lua space. This is intended to replace + * math.randomseed and math.random, so we conform to their behavior. */ + +namespace +{ + MersenneTwister g_LuaPRNG; + + /* To map from [0..2^32-1] to [0..1), we divide by 2^32. */ + const double DIVISOR = pow( double(2), double(32) ); + + static int Seed( lua_State *L ) + { + g_LuaPRNG.Reset( IArg(1) ); + return 0; + } + + static int Random( lua_State *L ) + { + switch( lua_gettop(L) ) + { + /* [0..1) */ + case 0: + { + double r = double(g_LuaPRNG()) / DIVISOR; + lua_pushnumber( L, r ); + return 1; + } + + /* [1..u] */ + case 1: + { + int upper = IArg(1); + luaL_argcheck( L, 1 <= upper, 1, "interval is empty" ); + lua_pushnumber( L, g_LuaPRNG(upper) + 1 ); + return 1; + } + /* [l..u] */ + case 2: + { + int lower = IArg(1); + int upper = IArg(2); + luaL_argcheck( L, lower < upper, 2, "interval is empty" ); + lua_pushnumber( L, (int(g_LuaPRNG()) % (upper-lower+1)) + lower ); + return 1; + } + + /* wrong amount of arguments */ + default: + { + return luaL_error( L, "wrong number of arguments" ); + } + } + } + + const luaL_Reg MersenneTwisterTable[] = + { + LIST_METHOD( Seed ), + LIST_METHOD( Random ), + { NULL, NULL } + }; +} + +LUA_REGISTER_NAMESPACE( MersenneTwister ); + +void fapproach( float& val, float other_val, float to_move ) +{ + ASSERT_M( to_move >= 0, ssprintf("to_move: %f < 0", to_move) ); + if( val == other_val ) + return; + float fDelta = other_val - val; + float fSign = fDelta / fabsf( fDelta ); + float fToMove = fSign*to_move; + if( fabsf(fToMove) > fabsf(fDelta) ) + fToMove = fDelta; // snap + val += fToMove; +} + +/* Return a positive x mod y. */ +float fmodfp(float x, float y) +{ + x = fmodf(x, y); /* x is [-y,y] */ + x += y; /* x is [0,y*2] */ + x = fmodf(x, y); /* x is [0,y] */ + return x; +} + +int power_of_two( int iInput ) +{ + int iValue = 1; + + while( iValue < iInput ) + iValue <<= 1; + + return iValue; +} + +bool IsAnInt( const RString &s ) +{ + if( !s.size() ) + return false; + + for( size_t i=0; i < s.size(); ++i ) + if( s[i] < '0' || s[i] > '9' ) + return false; + + return true; +} + +bool IsHexVal( const RString &s ) +{ + if( !s.size() ) + return false; + + for( size_t i=0; i < s.size(); ++i ) + if( !(s[i] >= '0' && s[i] <= '9') && + !(toupper(s[i]) >= 'A' && toupper(s[i]) <= 'F')) + return false; + + return true; +} + +RString BinaryToHex( const void *pData_, int iNumBytes ) +{ + const unsigned char *pData = (const unsigned char *) pData_; + RString s; + for( int i=0; i arrayBits; + split( sHHMMSS, ":", arrayBits, false ); + + while( arrayBits.size() < 3 ) + arrayBits.insert(arrayBits.begin(), "0" ); // pad missing bits + + float fSeconds = 0; + fSeconds += StringToInt( arrayBits[0] ) * 60 * 60; + fSeconds += StringToInt( arrayBits[1] ) * 60; + fSeconds += StringToFloat( arrayBits[2] ); + + return fSeconds; +} + +RString SecondsToHHMMSS( float fSecs ) +{ + const int iMinsDisplay = (int)fSecs/60; + const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; + RString sReturn = ssprintf( "%02d:%02d:%02d", iMinsDisplay/60, iMinsDisplay%60, iSecsDisplay ); + return sReturn; +} + +RString SecondsToMMSSMsMs( float fSecs ) +{ + const int iMinsDisplay = (int)fSecs/60; + const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; + const int iLeftoverDisplay = (int) ( (fSecs - iMinsDisplay*60 - iSecsDisplay) * 100 ); + RString sReturn = ssprintf( "%02d:%02d.%02d", iMinsDisplay, iSecsDisplay, min(99,iLeftoverDisplay) ); + return sReturn; +} + +RString SecondsToMSSMsMs( float fSecs ) +{ + const int iMinsDisplay = (int)fSecs/60; + const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; + const int iLeftoverDisplay = (int) ( (fSecs - iMinsDisplay*60 - iSecsDisplay) * 100 ); + RString sReturn = ssprintf( "%01d:%02d.%02d", iMinsDisplay, iSecsDisplay, min(99,iLeftoverDisplay) ); + return sReturn; +} + +RString SecondsToMMSSMsMsMs( float fSecs ) +{ + const int iMinsDisplay = (int)fSecs/60; + const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; + const int iLeftoverDisplay = (int) ( (fSecs - iMinsDisplay*60 - iSecsDisplay) * 1000 ); + RString sReturn = ssprintf( "%02d:%02d.%03d", iMinsDisplay, iSecsDisplay, min(999,iLeftoverDisplay) ); + return sReturn; +} + +RString SecondsToMSS( float fSecs ) +{ + const int iMinsDisplay = (int)fSecs/60; + const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; + RString sReturn = ssprintf( "%01d:%02d", iMinsDisplay, iSecsDisplay); + return sReturn; +} + +RString SecondsToMMSS( float fSecs ) +{ + const int iMinsDisplay = (int)fSecs/60; + const int iSecsDisplay = (int)fSecs - iMinsDisplay*60; + RString sReturn = ssprintf( "%02d:%02d", iMinsDisplay, iSecsDisplay); + return sReturn; +} + +RString PrettyPercent( float fNumerator, float fDenominator) +{ + return ssprintf("%0.2f%%",fNumerator/fDenominator*100); +} + +RString Commify( int iNum ) +{ + RString sNum = ssprintf("%d",iNum); + return Commify( sNum ); +} + +RString Commify( RString sNum, RString sSeperator ) +{ + RString sReturn; + for( unsigned i=0; i= 3 && iPercent > 2 && sStr[iEnd-2] == 'l' && sStr[iEnd-1] == 'l' ) + { + sRet.append( sStr, iPercent, iEnd - iPercent - 2 ); // % + sRet.append( "I64" ); // %I64 + sRet.append( sStr, iEnd, 1 ); // %I64i + iOffset = iEnd + 1; + } + else + { + if( iEnd == sStr.npos ) + iEnd = sStr.size() - 1; + sRet.append( sStr, iOffset, iEnd - iOffset + 1 ); + iOffset = iEnd + 1; + } + } + return sRet; +} +#else +RString ConvertI64FormatString( const RString &sStr ) { return sStr; } +#endif + +/* ISO-639-1 codes: http://www.loc.gov/standards/iso639-2/php/code_list.php + * native forms: http://people.w3.org/rishida/names/languages.html + * We don't use 3-letter codes, so we don't bother supporting them. */ +static const LanguageInfo g_langs[] = +{ + {"aa", "Afar"}, + {"ab", "Abkhazian"}, + {"af", "Afrikaans"}, + {"am", "Amharic"}, + {"ar", "Arabic"}, + {"as", "Assamese"}, + {"ay", "Aymara"}, + {"az", "Azerbaijani"}, + {"ba", "Bashkir"}, + {"be", "Byelorussian"}, + {"bg", "Bulgarian"}, + {"bh", "Bihari"}, + {"bi", "Bislama"}, + {"bn", "Bengali"}, + {"bo", "Tibetan"}, + {"br", "Breton"}, + {"ca", "Catalan"}, + {"co", "Corsican"}, + {"cs", "Czech"}, + {"cy", "Welsh"}, + {"da", "Danish"}, + {"de", "German"}, + {"dz", "Bhutani"}, + {"el", "Greek"}, + {"en", "English"}, + {"eo", "Esperanto"}, + {"es", "Spanish"}, + {"et", "Estonian"}, + {"eu", "Basque"}, + {"fa", "Persian"}, + {"fi", "Finnish"}, + {"fj", "Fiji"}, + {"fo", "Faeroese"}, + {"fr", "French"}, + {"fy", "Frisian"}, + {"ga", "Irish"}, + {"gd", "Gaelic"}, + {"gl", "Galician"}, + {"gn", "Guarani"}, + {"gu", "Gujarati"}, + {"ha", "Hausa"}, + {"he", "Hebrew"}, + {"hi", "Hindi"}, + {"hr", "Croatian"}, + {"hu", "Hungarian"}, + {"hy", "Armenian"}, + {"ia", "Interlingua"}, + {"id", "Indonesian"}, + {"ie", "Interlingue"}, + {"ik", "Inupiak"}, + {"in", "Indonesian"}, // compatibility + {"is", "Icelandic"}, + {"it", "Italian"}, + {"iw", "Hebrew"}, // compatibility + {"ja", "Japanese"}, + {"ji", "Yiddish"}, // compatibility + {"jw", "Javanese"}, + {"ka", "Georgian"}, + {"kk", "Kazakh"}, + {"kl", "Greenlandic"}, + {"km", "Cambodian"}, + {"kn", "Kannada"}, + {"ko", "Korean"}, + {"ks", "Kashmiri"}, + {"ku", "Kurdish"}, + {"ky", "Kirghiz"}, + {"la", "Latin"}, + {"ln", "Lingala"}, + {"lo", "Laothian"}, + {"lt", "Lithuanian"}, + {"lv", "Latvian"}, + {"mg", "Malagasy"}, + {"mi", "Maori"}, + {"mk", "Macedonian"}, + {"ml", "Malayalam"}, + {"mn", "Mongolian"}, + {"mo", "Moldavian"}, + {"mr", "Marathi"}, + {"ms", "Malay"}, + {"mt", "Maltese"}, + {"my", "Burmese"}, + {"na", "Nauru"}, + {"ne", "Nepali"}, + {"nl", "Dutch"}, + {"no", "Norwegian"}, + {"oc", "Occitan"}, + {"om", "Oromo"}, + {"or", "Oriya"}, + {"pa", "Punjabi"}, + {"pl", "Polish"}, + {"ps", "Pashto"}, + {"pt", "Portuguese"}, + {"qu", "Quechua"}, + {"rm", "Rhaeto-Romance"}, + {"rn", "Kirundi"}, + {"ro", "Romanian"}, + {"ru", "Russian"}, + {"rw", "Kinyarwanda"}, + {"sa", "Sanskrit"}, + {"sd", "Sindhi"}, + {"sg", "Sangro"}, + {"sh", "Serbo-Croatian"}, + {"si", "Singhalese"}, + {"sk", "Slovak"}, + {"sl", "Slovenian"}, + {"sm", "Samoan"}, + {"sn", "Shona"}, + {"so", "Somali"}, + {"sq", "Albanian"}, + {"sr", "Serbian"}, + {"ss", "Siswati"}, + {"st", "Sesotho"}, + {"su", "Sudanese"}, + {"sv", "Swedish"}, + {"sw", "Swahili"}, + {"ta", "Tamil"}, + {"te", "Tegulu"}, + {"tg", "Tajik"}, + {"th", "Thai"}, + {"ti", "Tigrinya"}, + {"tk", "Turkmen"}, + {"tl", "Tagalog"}, + {"tn", "Setswana"}, + {"to", "Tonga"}, + {"tr", "Turkish"}, + {"ts", "Tsonga"}, + {"tt", "Tatar"}, + {"tw", "Twi"}, + {"uk", "Ukrainian"}, + {"ur", "Urdu"}, + {"uz", "Uzbek"}, + {"vi", "Vietnamese"}, + {"vo", "Volapuk"}, + {"wo", "Wolof"}, + {"xh", "Xhosa"}, + {"yi", "Yiddish"}, + {"yo", "Yoruba"}, + {"zh", "Chinese"}, + {"zu", "Zulu"}, +}; + +void GetLanguageInfos( vector &vAddTo ) +{ + for( unsigned i=0; i &sSource) +{ + if( sSource.empty() ) + return RString(); + + RString sTmp; + + for( unsigned iNum = 0; iNum < sSource.size()-1; iNum++ ) + { + sTmp += sSource[iNum]; + sTmp += sDeliminator; + } + sTmp += sSource.back(); + return sTmp; +} + +RString join( const RString &sDelimitor, vector::const_iterator begin, vector::const_iterator end ) +{ + if( begin == end ) + return RString(); + + RString sRet; + while( begin != end ) + { + sRet += *begin; + ++begin; + if( begin != end ) + sRet += sDelimitor; + } + + return sRet; +} + +RString SmEscape( const RString &sUnescaped ) +{ + return SmEscape( sUnescaped.c_str(), sUnescaped.size() ); +} + +RString SmEscape( const char *cUnescaped, int len ) +{ + RString answer = ""; + for( int i = 0; i < len; ++i ) + { + // Other characters we could theoretically escape: + // NotesWriterSM.cpp used to claim ',' should be escaped, but there was no explanation why + // '#' is both a control character and a valid part of a parameter. The only way for there to be + // any confusion is in a misformatted .sm file, though, so it is unnecessary to escape it. + if( cUnescaped[i] == '/' && i + 1 < len && cUnescaped[i + 1] == '/' ) + { + answer += "\\/\\/"; + ++i; // increment here so we skip both //s + continue; + } + if( cUnescaped[i] == '\\' || cUnescaped[i] == ':' || cUnescaped[i] == ';' ) + answer += "\\"; + answer += cUnescaped[i]; + } + return answer; +} + +RString DwiEscape( const RString &sUnescaped ) +{ + return DwiEscape( sUnescaped.c_str(), sUnescaped.size() ); +} + +RString DwiEscape( const char *cUnescaped, int len ) +{ + RString answer = ""; + for( int i = 0; i < len; ++i ) + { + switch( cUnescaped[i] ) + { + // TODO: Which of these characters actually affect DWI? + case '\\': + case ':': + case ';': answer += '|'; break; + case '[': answer += '('; break; + case ']': answer += ')'; break; + default: answer += cUnescaped[i]; + } + } + return answer; +} + +template +static int DelimitorLength( const S &Delimitor ) +{ + return Delimitor.size(); +} + +static int DelimitorLength( char Delimitor ) +{ + return 1; +} + +static int DelimitorLength( wchar_t Delimitor ) +{ + return 1; +} + +template +void do_split( const S &Source, const C Delimitor, vector &AddIt, const bool bIgnoreEmpty ) +{ + /* Short-circuit if the source is empty; we want to return an empty vector if + * the string is empty, even if bIgnoreEmpty is true. */ + if( Source.empty() ) + return; + + size_t startpos = 0; + + do { + size_t pos; + pos = Source.find( Delimitor, startpos ); + if( pos == Source.npos ) + pos = Source.size(); + + if( pos-startpos > 0 || !bIgnoreEmpty ) + { + /* Optimization: if we're copying the whole string, avoid substr; this + * allows this copy to be refcounted, which is much faster. */ + if( startpos == 0 && pos-startpos == Source.size() ) + AddIt.push_back(Source); + else + { + const S AddRString = Source.substr(startpos, pos-startpos); + AddIt.push_back(AddRString); + } + } + + startpos = pos+DelimitorLength(Delimitor); + } while ( startpos <= Source.size() ); +} + +void split( const RString &sSource, const RString &sDelimitor, vector &asAddIt, const bool bIgnoreEmpty ) +{ + if( sDelimitor.size() == 1 ) + do_split( sSource, sDelimitor[0], asAddIt, bIgnoreEmpty ); + else + do_split( sSource, sDelimitor, asAddIt, bIgnoreEmpty ); +} + +void split( const wstring &sSource, const wstring &sDelimitor, vector &asAddIt, const bool bIgnoreEmpty ) +{ + if( sDelimitor.size() == 1 ) + do_split( sSource, sDelimitor[0], asAddIt, bIgnoreEmpty ); + else + do_split( sSource, sDelimitor, asAddIt, bIgnoreEmpty ); +} + +/* Use: + +RString str="a,b,c"; +int start = 0, size = -1; +while( 1 ) +{ + do_split( str, ",", start, size ); + if( start == str.size() ) + break; + str[start] = 'Q'; +} + +*/ + +template +void do_split( const S &Source, const S &Delimitor, int &begin, int &size, int len, const bool bIgnoreEmpty ) +{ + if( size != -1 ) + { + // Start points to the beginning of the last delimiter. Move it up. + begin += size+Delimitor.size(); + begin = min( begin, len ); + } + + size = 0; + + if( bIgnoreEmpty ) + { + // Skip delims. + while( begin + Delimitor.size() < Source.size() && + !Source.compare( begin, Delimitor.size(), Delimitor ) ) + ++begin; + } + + /* Where's the string function to find within a substring? + * C++ strings apparently are missing that ... */ + size_t pos; + if( Delimitor.size() == 1 ) + pos = Source.find( Delimitor[0], begin ); + else + pos = Source.find( Delimitor, begin ); + if( pos == Source.npos || (int) pos > len ) + pos = len; + size = pos - begin; +} + +void split( const RString &Source, const RString &Delimitor, int &begin, int &size, int len, const bool bIgnoreEmpty ) +{ + do_split( Source, Delimitor, begin, size, len, bIgnoreEmpty ); +} + +void split( const wstring &Source, const wstring &Delimitor, int &begin, int &size, int len, const bool bIgnoreEmpty ) +{ + do_split( Source, Delimitor, begin, size, len, bIgnoreEmpty ); +} + +void split( const RString &Source, const RString &Delimitor, int &begin, int &size, const bool bIgnoreEmpty ) +{ + do_split( Source, Delimitor, begin, size, Source.size(), bIgnoreEmpty ); +} + +void split( const wstring &Source, const wstring &Delimitor, int &begin, int &size, const bool bIgnoreEmpty ) +{ + do_split( Source, Delimitor, begin, size, Source.size(), bIgnoreEmpty ); +} + +/* + * foo\fum\ -> "foo\fum\", "", "" + * c:\foo\bar.txt -> "c:\foo\", "bar", ".txt" + * \\foo\fum -> "\\foo\", "fum", "" + */ +void splitpath( const RString &sPath, RString &sDir, RString &sFilename, RString &sExt ) +{ + sDir = sFilename = sExt = ""; + + vector asMatches; + + /* + * One level of escapes for the regex, one for C. Ew. + * This is really: + * ^(.*[\\/])?(.*)$ + */ + static Regex sep("^(.*[\\\\/])?(.*)$"); + bool bCheck = sep.Compare( sPath, asMatches ); + ASSERT( bCheck ); + + sDir = asMatches[0]; + const RString sBase = asMatches[1]; + + /* ^(.*)(\.[^\.]+)$ */ + static Regex SplitExt("^(.*)(\\.[^\\.]+)$"); + if( SplitExt.Compare(sBase, asMatches) ) + { + sFilename = asMatches[0]; + sExt = asMatches[1]; + } + else + { + sFilename = sBase; + } +} + +/* "foo.bar", "baz" -> "foo.baz" + * "foo", "baz" -> "foo.baz" + * "foo.bar", "" -> "foo" */ +RString SetExtension( const RString &sPath, const RString &sExt ) +{ + RString sDir, sFileName, sOldExt; + splitpath( sPath, sDir, sFileName, sOldExt ); + return sDir + sFileName + (sExt.size()? ".":"") + sExt; +} + +RString GetExtension( const RString &sPath ) +{ + size_t pos = sPath.rfind( '.' ); + if( pos == sPath.npos ) + return RString(); + + size_t slash = sPath.find( '/', pos ); + if( slash != sPath.npos ) + return RString(); /* rare: path/dir.ext/fn */ + + return sPath.substr( pos+1, sPath.size()-pos+1 ); +} + +RString GetFileNameWithoutExtension( const RString &sPath ) +{ + RString sThrowAway, sFName; + splitpath( sPath, sThrowAway, sFName, sThrowAway ); + return sFName; +} + +void MakeValidFilename( RString &sName ) +{ + wstring wsName = RStringToWstring( sName ); + wstring wsInvalid = L"/\\:*?\"<>|"; + for( unsigned i = 0; i < wsName.size(); ++i ) + { + wchar_t w = wsName[i]; + if( w >= 32 && + w < 126 && + wsInvalid.find_first_of(w) == wsInvalid.npos ) + continue; + + if( w == L'"' ) + { + wsName[i] = L'\''; + continue; + } + + /* We could replace with closest matches in ASCII: convert the character + * to UTF-8 NFD (decomposed) (maybe NFKD?), and see if the first + * character is ASCII. This is useless for non-Western languages, + * since we'll replace the whole filename. */ + wsName[i] = '_'; + } + + sName = WStringToRString( wsName ); +} + +int g_argc = 0; +char **g_argv = NULL; + +void SetCommandlineArguments( int argc, char **argv ) +{ + g_argc = argc; + g_argv = argv; +} + +void GetCommandLineArguments( int &argc, char **&argv ) +{ + argc = g_argc; + argv = g_argv; +} + +/* Search for the commandline argument given; eg. "test" searches for the + * option "--test". All commandline arguments are getopt_long style: --foo; + * short arguments (-x) are not supported. (These are not intended for + * common, general use, so having short options isn't currently needed.) + * If argument is non-NULL, accept an argument. */ +bool GetCommandlineArgument( const RString &option, RString *argument, int iIndex ) +{ + const RString optstr = "--" + option; + + for( int arg = 1; arg < g_argc; ++arg ) + { + const RString CurArgument = g_argv[arg]; + + const size_t i = CurArgument.find( "=" ); + RString CurOption = CurArgument.substr(0,i); + if( CurOption.CompareNoCase(optstr) ) + continue; // no match + + // Found it. + if( iIndex ) + { + --iIndex; + continue; + } + + if( argument ) + { + if( i != RString::npos ) + *argument = CurArgument.substr( i+1 ); + else + *argument = ""; + } + + return true; + } + + return false; +} + +RString GetCwd() +{ + char buf[PATH_MAX]; + bool ret = getcwd(buf, PATH_MAX) != NULL; + ASSERT(ret); + return buf; +} + +/* + * Calculate a standard CRC32. iCRC should be initialized to 0. + * References: + * http://www.theorem.com/java/CRC32.java, + * http://www.faqs.org/rfcs/rfc1952.html + */ +void CRC32( unsigned int &iCRC, const void *pVoidBuffer, size_t iSize ) +{ + static unsigned tab[256]; + static bool initted = false; + if( !initted ) + { + initted = true; + const unsigned POLY = 0xEDB88320; + + for( int i = 0; i < 256; ++i ) + { + tab[i] = i; + for( int j = 0; j < 8; ++j ) + { + if( tab[i] & 1 ) + tab[i] = (tab[i] >> 1) ^ POLY; + else + tab[i] >>= 1; + } + } + } + + iCRC ^= 0xFFFFFFFF; + + const char *pBuffer = (const char *) pVoidBuffer; + for( unsigned i = 0; i < iSize; ++i ) + iCRC = (iCRC >> 8) ^ tab[(iCRC ^ pBuffer[i]) & 0xFF]; + + iCRC ^= 0xFFFFFFFF; +} + +unsigned int GetHashForString( const RString &s ) +{ + unsigned crc = 0; + CRC32( crc, s.data(), s.size() ); + return crc; +} + +/* Return true if "dir" is empty or does not exist. */ +bool DirectoryIsEmpty( const RString &sDir ) +{ + if( sDir.empty() ) + return true; + if( !DoesFileExist(sDir) ) + return true; + + vector asFileNames; + GetDirListing( sDir, asFileNames ); + return asFileNames.empty(); +} + +bool CompareRStringsAsc( const RString &sStr1, const RString &sStr2 ) +{ + return sStr1.CompareNoCase( sStr2 ) < 0; +} + +bool CompareRStringsDesc( const RString &sStr1, const RString &sStr2 ) +{ + return sStr1.CompareNoCase( sStr2 ) > 0; +} + +void SortRStringArray( vector &arrayRStrings, const bool bSortAscending ) +{ + sort( arrayRStrings.begin(), arrayRStrings.end(), + bSortAscending?CompareRStringsAsc:CompareRStringsDesc ); +} + +float calc_mean( const float *pStart, const float *pEnd ) +{ + return accumulate( pStart, pEnd, 0.f ) / distance( pStart, pEnd ); +} + +float calc_stddev( const float *pStart, const float *pEnd, bool bSample ) +{ + /* Calculate the mean. */ + float fMean = calc_mean( pStart, pEnd ); + + /* Calculate stddev. */ + float fDev = 0.0f; + for( const float *i=pStart; i != pEnd; ++i ) + fDev += (*i - fMean) * (*i - fMean); + fDev /= distance( pStart, pEnd ) - (bSample ? 1 : 0); + fDev = sqrtf( fDev ); + + return fDev; +} + +bool CalcLeastSquares( const vector< pair > &vCoordinates, + float &fSlope, float &fIntercept, float &fError ) +{ + if( vCoordinates.empty() ) + return false; + float fSumXX = 0.0f, fSumXY = 0.0f, fSumX = 0.0f, fSumY = 0.0f; + for( unsigned i = 0; i < vCoordinates.size(); ++i ) + { + fSumXX += vCoordinates[i].first * vCoordinates[i].first; + fSumXY += vCoordinates[i].first * vCoordinates[i].second; + fSumX += vCoordinates[i].first; + fSumY += vCoordinates[i].second; + } + const float fDenominator = vCoordinates.size() * fSumXX - fSumX * fSumX; + fSlope = (vCoordinates.size() * fSumXY - fSumX * fSumY) / fDenominator; + fIntercept = (fSumXX * fSumY - fSumX * fSumXY) / fDenominator; + + fError = 0.0f; + for( unsigned i = 0; i < vCoordinates.size(); ++i ) + { + const float fOneError = fIntercept + fSlope * vCoordinates[i].first - vCoordinates[i].second; + fError += fOneError * fOneError; + } + fError /= vCoordinates.size(); + fError = sqrtf( fError ); + return true; +} + +void FilterHighErrorPoints( vector< pair > &vCoordinates, + float fSlope, float fIntercept, float fCutoff ) +{ + unsigned int iOut = 0; + for( unsigned int iIn = 0; iIn < vCoordinates.size(); ++iIn ) + { + const float fError = fIntercept + fSlope * vCoordinates[iIn].first - vCoordinates[iIn].second; + if( fabsf(fError) < fCutoff ) + { + vCoordinates[iOut] = vCoordinates[iIn]; + ++iOut; + } + } + vCoordinates.resize( iOut ); +} + +void TrimLeft( RString &sStr, const char *s ) +{ + int n = 0; + while( n < int(sStr.size()) && strchr(s, sStr[n]) ) + n++; + + sStr.erase( sStr.begin(), sStr.begin()+n ); +} + +void TrimRight( RString &sStr, const char *s ) +{ + int n = sStr.size(); + while( n > 0 && strchr(s, sStr[n-1]) ) + n--; + + /* Delete from n to the end. If n == sStr.size(), nothing is deleted; + * if n == 0, the whole string is erased. */ + sStr.erase( sStr.begin()+n, sStr.end() ); +} + +void Trim( RString &sStr, const char *s ) +{ + RString::size_type b = 0, e = sStr.size(); + while( b < e && strchr(s, sStr[b]) ) + ++b; + while( b < e && strchr(s, sStr[e-1]) ) + --e; + sStr.assign( sStr.substr(b, e-b) ); +} + +void StripCrnl( RString &s ) +{ + while( s.size() && (s[s.size()-1] == '\r' || s[s.size()-1] == '\n') ) + s.erase( s.size()-1 ); +} + +bool BeginsWith( const RString &sTestThis, const RString &sBeginning ) +{ + ASSERT( !sBeginning.empty() ); + return sTestThis.compare( 0, sBeginning.length(), sBeginning ) == 0; +} + +bool EndsWith( const RString &sTestThis, const RString &sEnding ) +{ + ASSERT( !sEnding.empty() ); + if( sTestThis.size() < sEnding.size() ) + return false; + return sTestThis.compare( sTestThis.length()-sEnding.length(), sEnding.length(), sEnding ) == 0; +} + +RString URLEncode( const RString &sStr ) +{ + RString sOutput; + for( unsigned k = 0; k < sStr.size(); k++ ) + { + char t = sStr[k]; + if( t >= '!' && t <= 'z' ) + sOutput += t; + else + sOutput += "%" + ssprintf( "%02X", t ); + } + return sOutput; +} + +// remove various version control-related files +static bool CVSOrSVN( const RString& s ) +{ + return s.Right(3).EqualsNoCase("CVS") || + s.Right(4) == ".svn" || + s.Right(3).EqualsNoCase(".hg"); +} + +void StripCvsAndSvn( vector &vs ) +{ + RemoveIf( vs, CVSOrSVN ); +} + +static bool MacResourceFork( const RString& s ) +{ + return s.Left(2).EqualsNoCase("._"); +} + +void StripMacResourceForks( vector &vs ) +{ + RemoveIf( vs, MacResourceFork ); +} + +// path is a .redir pathname. Read it and return the real one. +RString DerefRedir( const RString &_path ) +{ + RString sPath = _path; + + for( int i=0; i<100; i++ ) + { + if( GetExtension(sPath) != "redir" ) + return sPath; + + RString sNewFileName; + GetFileContents( sPath, sNewFileName, true ); + + // Empty is invalid. + if( sNewFileName == "" ) + return RString(); + + RString sPath2 = Dirname(sPath) + sNewFileName; + + CollapsePath( sPath2 ); + + sPath2 += "*"; + + vector matches; + GetDirListing( sPath2, matches, false, true ); + + if( matches.empty() ) + RageException::Throw( "The redirect \"%s\" references a file \"%s\" which doesn't exist.", sPath.c_str(), sPath2.c_str() ); + else if( matches.size() > 1 ) + RageException::Throw( "The redirect \"%s\" references a file \"%s\" with multiple matches.", sPath.c_str(), sPath2.c_str() ); + + sPath = matches[0]; + } + + RageException::Throw( "Circular redirect \"%s\".", sPath.c_str() ); +} + +bool GetFileContents( const RString &sPath, RString &sOut, bool bOneLine ) +{ + // Don't warn if the file doesn't exist, but do warn if it exists and fails to open. + if( !IsAFile(sPath) ) + return false; + + RageFile file; + if( !file.Open(sPath) ) + { + LOG->Warn( "GetFileContents(%s): %s", sPath.c_str(), file.GetError().c_str() ); + return false; + } + + // todo: figure out how to make this UTF-8 safe. -aj + RString sData; + int iGot; + if( bOneLine ) + iGot = file.GetLine( sData ); + else + iGot = file.Read( sData, file.GetFileSize() ); + + if( iGot == -1 ) + { + LOG->Warn( "GetFileContents(%s): %s", sPath.c_str(), file.GetError().c_str() ); + return false; + } + + if( bOneLine ) + StripCrnl( sData ); + + sOut = sData; + return true; +} + +bool GetFileContents( const RString &sFile, vector &asOut ) +{ + RageFile file; + if( !file.Open(sFile) ) + { + LOG->Warn( "GetFileContents(%s): %s", sFile.c_str(), file.GetError().c_str() ); + return false; + } + + RString sLine; + while( file.GetLine(sLine) ) + asOut.push_back( sLine ); + return true; +} + +#include "../extern/pcre/pcre.h" +void Regex::Compile() +{ + const char *error; + int offset; + m_pReg = pcre_compile( m_sPattern.c_str(), PCRE_CASELESS, &error, &offset, NULL ); + + if( m_pReg == NULL ) + RageException::Throw( "Invalid regex: \"%s\" (%s).", m_sPattern.c_str(), error ); + + int iRet = pcre_fullinfo( (pcre *) m_pReg, NULL, PCRE_INFO_CAPTURECOUNT, &m_iBackrefs ); + ASSERT( iRet >= 0 ); + + ++m_iBackrefs; + ASSERT( m_iBackrefs < 128 ); +} + +void Regex::Set( const RString &sStr ) +{ + Release(); + m_sPattern = sStr; + Compile(); +} + +void Regex::Release() +{ + pcre_free( m_pReg ); + m_pReg = NULL; + m_sPattern = RString(); +} + +Regex::Regex( const RString &sStr ): m_pReg(NULL), m_iBackrefs(0), m_sPattern(RString()) +{ + Set( sStr ); +} + +Regex::Regex( const Regex &rhs ): m_pReg(NULL), m_iBackrefs(0), m_sPattern(RString()) +{ + Set( rhs.m_sPattern ); +} + +Regex &Regex::operator=( const Regex &rhs ) +{ + if( this != &rhs ) + Set( rhs.m_sPattern ); + return *this; +} + +Regex::~Regex() +{ + Release(); +} + +bool Regex::Compare( const RString &sStr ) +{ + int iMat[128*3]; + int iRet = pcre_exec( (pcre *) m_pReg, NULL, sStr.data(), sStr.size(), 0, 0, iMat, 128*3 ); + + if( iRet < -1 ) + RageException::Throw( "Unexpected return from pcre_exec('%s'): %i.", m_sPattern.c_str(), iRet ); + + return iRet >= 0; +} + +bool Regex::Compare( const RString &sStr, vector &asMatches ) +{ + asMatches.clear(); + + int iMat[128*3]; + int iRet = pcre_exec( (pcre *) m_pReg, NULL, sStr.data(), sStr.size(), 0, 0, iMat, 128*3 ); + + if( iRet < -1 ) + RageException::Throw( "Unexpected return from pcre_exec('%s'): %i.", m_sPattern.c_str(), iRet ); + + if( iRet == -1 ) + return false; + + for( unsigned i = 1; i < m_iBackrefs; ++i ) + { + const int iStart = iMat[i*2], end = iMat[i*2+1]; + if( iStart == -1 ) + asMatches.push_back( RString() ); /* no match */ + else + asMatches.push_back( sStr.substr(iStart, end - iStart) ); + } + + return true; +} + +// Arguments and behavior are the same are similar to +// http://us3.php.net/manual/en/function.preg-replace.php +bool Regex::Replace( const RString &sReplacement, const RString &sSubject, RString &sOut ) +{ + vector asMatches; + if( !Compare(sSubject, asMatches) ) + return false; + + sOut = sReplacement; + + // TODO: optimize me by iterating only once over the string + for( unsigned i=0; i= s.size() ) + return false; + + if( is_utf8_continuation_byte( s[start] ) || /* misplaced continuation byte */ + (s[start] & 0xFE) == 0xFE ) /* 0xFE, 0xFF */ + { + start += 1; + return false; + } + + int len = utf8_get_char_len( s[start] ); + + const int first_byte_mask[] = { -1, 0x7F, 0x1F, 0x0F, 0x07, 0x03, 0x01 }; + + ch = wchar_t(s[start] & first_byte_mask[len]); + + for( int i = 1; i < len; ++i ) + { + if( start+i >= s.size() ) + { + /* We expected a continuation byte, but didn't get one. Return error, and point + * start at the unexpected byte; it's probably a new sequence. */ + start += i; + return false; + } + + char byte = s[start+i]; + if( !is_utf8_continuation_byte(byte) ) + { + /* We expected a continuation byte, but didn't get one. Return error, and point + * start at the unexpected byte; it's probably a new sequence. */ + start += i; + return false; + } + ch = (ch << 6) | (byte & 0x3F); + } + + bool bValid = true; + { + unsigned c1 = (unsigned) s[start] & 0xFF; + unsigned c2 = (unsigned) s[start+1] & 0xFF; + int c = (c1 << 8) + c2; + if( (c & 0xFE00) == 0xC000 || + (c & 0xFFE0) == 0xE080 || + (c & 0xFFF0) == 0xF080 || + (c & 0xFFF8) == 0xF880 || + (c & 0xFFFC) == 0xFC80 ) + { + bValid = false; + } + } + + if( ch == 0xFFFE || ch == 0xFFFF ) + bValid = false; + + start += len; + return bValid; +} + +/* Like utf8_to_wchar_ec, but only does enough error checking to prevent crashing. */ +bool utf8_to_wchar( const char *s, size_t iLength, unsigned &start, wchar_t &ch ) +{ + if( start >= iLength ) + return false; + + int len = utf8_get_char_len( s[start] ); + + if( start+len > iLength ) + { + // We don't have room for enough continuation bytes. Return error. + start += len; + ch = L'?'; + return false; + } + + switch( len ) + { + case 1: + ch = (s[start+0] & 0x7F); + break; + case 2: + ch = ( (s[start+0] & 0x1F) << 6 ) | + (s[start+1] & 0x3F); + break; + case 3: + ch = ( (s[start+0] & 0x0F) << 12 ) | + ( (s[start+1] & 0x3F) << 6 ) | + (s[start+2] & 0x3F); + break; + case 4: + ch = ( (s[start+0] & 0x07) << 18 ) | + ( (s[start+1] & 0x3F) << 12 ) | + ( (s[start+2] & 0x3F) << 6 ) | + (s[start+3] & 0x3F); + break; + case 5: + ch = ( (s[start+0] & 0x03) << 24 ) | + ( (s[start+1] & 0x3F) << 18 ) | + ( (s[start+2] & 0x3F) << 12 ) | + ( (s[start+3] & 0x3F) << 6 ) | + (s[start+4] & 0x3F); + break; + + case 6: + ch = ( (s[start+0] & 0x01) << 30 ) | + ( (s[start+1] & 0x3F) << 24 ) | + ( (s[start+2] & 0x3F) << 18 ) | + ( (s[start+3] & 0x3F) << 12) | + ( (s[start+4] & 0x3F) << 6 ) | + (s[start+5] & 0x3F); + break; + + } + + start += len; + return true; +} + + +// UTF-8 encode ch and append to out. +void wchar_to_utf8( wchar_t ch, RString &out ) +{ + if( ch < 0x80 ) { out.append( 1, (char) ch ); return; } + + int cbytes = 0; + if( ch < 0x800 ) cbytes = 1; + else if( ch < 0x10000 ) cbytes = 2; + else if( ch < 0x200000 ) cbytes = 3; + else if( ch < 0x4000000 ) cbytes = 4; + else cbytes = 5; + + { + int shift = cbytes*6; + const int init_masks[] = { 0xC0, 0xE0, 0xF0, 0xF8, 0xFC }; + out.append( 1, (char) (init_masks[cbytes-1] | (ch>>shift)) ); + } + + for( int i = 0; i < cbytes; ++i ) + { + int shift = (cbytes-i-1)*6; + out.append( 1, (char) (0x80 | ((ch>>shift)&0x3F)) ); + } +} + +wchar_t utf8_get_char( const RString &s ) +{ + unsigned start = 0; + wchar_t ret; + if( !utf8_to_wchar_ec( s, start, ret ) ) + return INVALID_CHAR; + return ret; +} + +// Replace invalid sequences in s. +void utf8_sanitize( RString &s ) +{ + RString ret; + for( unsigned start = 0; start < s.size(); ) + { + wchar_t ch; + if( !utf8_to_wchar_ec( s, start, ch ) ) + ch = INVALID_CHAR; + + wchar_to_utf8( ch, ret ); + } + + s = ret; +} + +bool utf8_is_valid( const RString &s ) +{ + for( unsigned start = 0; start < s.size(); ) + { + wchar_t ch; + if( !utf8_to_wchar_ec( s, start, ch ) ) + return false; + } + return true; +} + +/* Windows tends to drop garbage BOM characters at the start of UTF-8 text files. + * Remove them. */ +void utf8_remove_bom( RString &sLine ) +{ + if( !sLine.compare(0, 3, "\xef\xbb\xbf") ) + sLine.erase(0, 3); +} + +static int UnicodeDoUpper( char *p, size_t iLen, const unsigned char pMapping[256] ) +{ + // Note: this has problems with certain accented characters. -aj + wchar_t wc = L'\0'; + unsigned iStart = 0; + if( !utf8_to_wchar(p, iLen, iStart, wc) ) + return 1; + + wchar_t iUpper = wc; + if( wc < 256 ) + iUpper = pMapping[wc]; + if( iUpper != wc ) + { + RString sOut; + wchar_to_utf8( iUpper, sOut ); + if( sOut.size() == iStart ) + memcpy( p, sOut.data(), sOut.size() ); + else + WARN( ssprintf("UnicodeDoUpper: invalid character at \"%s\"", RString(p,iLen).c_str()) ); + } + + return iStart; +} + +/* Fast in-place MakeUpper and MakeLower. This only replaces characters with characters of the same UTF-8 + * length, so we never have to move the whole string. This is optimized for strings that have no + * non-ASCII characters. */ +void MakeUpper( char *p, size_t iLen ) +{ + char *pStart = p; + char *pEnd = p + iLen; + while( p < pEnd ) + { + // Fast path: + if( likely( !(*p & 0x80) ) ) + { + if( unlikely(*p >= 'a' && *p <= 'z') ) + *p += 'A' - 'a'; + ++p; + continue; + } + + int iRemaining = iLen - (p-pStart); + p += UnicodeDoUpper( p, iRemaining, g_UpperCase ); + } +} + +void MakeLower( char *p, size_t iLen ) +{ + char *pStart = p; + char *pEnd = p + iLen; + while( p < pEnd ) + { + // Fast path: + if( likely( !(*p & 0x80) ) ) + { + if( unlikely(*p >= 'A' && *p <= 'Z') ) + *p -= 'A' - 'a'; + ++p; + continue; + } + + int iRemaining = iLen - (p-pStart); + p += UnicodeDoUpper( p, iRemaining, g_LowerCase ); + } +} + +void UnicodeUpperLower( wchar_t *p, size_t iLen, const unsigned char pMapping[256] ) +{ + wchar_t *pEnd = p + iLen; + while( p != pEnd ) + { + if( *p < 256 ) + *p = pMapping[*p]; + ++p; + } +} + +void MakeUpper( wchar_t *p, size_t iLen ) +{ + UnicodeUpperLower( p, iLen, g_UpperCase ); +} + +void MakeLower( wchar_t *p, size_t iLen ) +{ + UnicodeUpperLower( p, iLen, g_LowerCase ); +} + +int StringToInt( const RString &sString ) +{ + int ret; + istringstream ( sString ) >> ret; + return ret; +} + +RString IntToString( const int &iNum ) +{ + stringstream ss; + ss << iNum; + return ss.str(); +} + +float StringToFloat( const RString &sString ) +{ + float ret = strtof( sString, NULL ); + + if( !isfinite(ret) ) + ret = 0.0f; + return ret; +} + +bool StringToFloat( const RString &sString, float &fOut ) +{ + char *endPtr; + + fOut = strtof( sString, &endPtr ); + return sString.size() && *endPtr == '\0' && isfinite( fOut ); +} + +RString FloatToString( const float &num ) +{ + stringstream ss; + ss << num; + return ss.str(); +} + +const wchar_t INVALID_CHAR = 0xFFFD; /* U+FFFD REPLACEMENT CHARACTER */ + +wstring RStringToWstring( const RString &s ) +{ + wstring ret; + ret.reserve( s.size() ); + for( unsigned start = 0; start < s.size(); ) + { + char c = s[start]; + if( !(c&0x80) ) + { + // ASCII fast path + ret += c; + ++start; + continue; + } + + wchar_t ch = L'\0'; + if( !utf8_to_wchar( s.data(), s.size(), start, ch ) ) + ch = INVALID_CHAR; + ret += ch; + } + + return ret; +} + +RString WStringToRString( const wstring &sStr ) +{ + RString sRet; + + for( unsigned i = 0; i < sStr.size(); ++i ) + wchar_to_utf8( sStr[i], sRet ); + + return sRet; +} + +RString WcharToUTF8( wchar_t c ) +{ + RString ret; + wchar_to_utf8( c, ret ); + return ret; +} + +// &a; -> a +void ReplaceEntityText( RString &sText, const map &m ) +{ + RString sRet; + + size_t iOffset = 0; + while( iOffset != sText.size() ) + { + size_t iStart = sText.find( '&', iOffset ); + if( iStart == sText.npos ) + { + // Optimization: if we didn't replace anything at all, do nothing. + if( iOffset == 0 ) + return; + + // Append the rest of the string. + sRet.append( sText, iOffset, sRet.npos ); + break; + } + + // Append the text between iOffset and iStart. + sRet.append( sText, iOffset, iStart-iOffset ); + iOffset += iStart-iOffset; + + // Optimization: stop early on "&", so "&&&&&&&&&&&" isn't n^2. + size_t iEnd = sText.find_first_of( "&;", iStart+1 ); + if( iEnd == sText.npos || sText[iEnd] == '&' ) + { + // & with no matching ;, or two & in a row. Append the & and continue. + sRet.append( sText, iStart, 1 ); + ++iOffset; + continue; + } + + RString sElement = sText.substr( iStart+1, iEnd-iStart-1 ); + sElement.MakeLower(); + + map::const_iterator it = m.find( sElement ); + if( it == m.end() ) + { + sRet.append( sText, iStart, iEnd-iStart+1 ); + iOffset = iEnd + 1; + continue; + } + + const RString &sTo = it->second; + sRet.append( sTo ); + iOffset = iEnd + 1; + } + + sText = sRet; +} + +// abcd -> &a; &b; &c; &d; +void ReplaceEntityText( RString &sText, const map &m ) +{ + RString sFind; + + FOREACHM_CONST( char, RString, m, c ) + sFind.append( 1, c->first ); + + RString sRet; + + size_t iOffset = 0; + while( iOffset != sText.size() ) + { + size_t iStart = sText.find_first_of( sFind, iOffset ); + if( iStart == sText.npos ) + { + // Optimization: if we didn't replace anything at all, do nothing. + if( iOffset == 0 ) + return; + + // Append the rest of the string. + sRet.append( sText, iOffset, sRet.npos ); + break; + } + + // Append the text between iOffset and iStart. + sRet.append( sText, iOffset, iStart-iOffset ); + iOffset += iStart-iOffset; + + char sElement = sText[iStart]; + + map::const_iterator it = m.find( sElement ); + ASSERT( it != m.end() ); + + const RString &sTo = it->second; + sRet.append( 1, '&' ); + sRet.append( sTo ); + sRet.append( 1, ';' ); + ++iOffset; + } + + sText = sRet; +} + +// Replace &#nnnn; (decimal) and &xnnnn; (hex) with corresponding UTF-8 characters. +void Replace_Unicode_Markers( RString &sText ) +{ + unsigned iStart = 0; + while( iStart < sText.size() ) + { + // Look for &#digits; + bool bHex = false; + size_t iPos = sText.find( "&#", iStart ); + if( iPos == sText.npos ) + { + bHex = true; + iPos = sText.find( "&x", iStart ); + } + + if( iPos == sText.npos ) + break; + iStart = iPos+1; + + unsigned p = iPos; + p += 2; + + // Found &# or &x. Is it followed by digits and a semicolon? + if( p >= sText.size() ) + continue; + + int iNumDigits = 0; + while( p < sText.size() && bHex? isxdigit(sText[p]):isdigit(sText[p]) ) + { + p++; + iNumDigits++; + } + + if( !iNumDigits ) + continue; // must have at least one digit + if( p >= sText.size() || sText[p] != ';' ) + continue; + p++; + + int iNum; + if( bHex ) + sscanf( sText.c_str()+iPos, "&x%x;", &iNum ); + else + sscanf( sText.c_str()+iPos, "&#%i;", &iNum ); + if( iNum > 0xFFFF ) + iNum = INVALID_CHAR; + + sText.replace( iPos, p-iPos, WcharToUTF8(wchar_t(iNum)) ); + } +} + +// Form a string to identify a wchar_t with ASCII. +RString WcharDisplayText( wchar_t c ) +{ + RString sChr; + sChr = ssprintf( "U+%4.4x", c ); + if( c < 128 ) + sChr += ssprintf( " ('%c')", char(c) ); + return sChr; +} + +/* Return the last named component of dir: + * a/b/c -> c + * a/b/c/ -> c + */ +RString Basename( const RString &sDir ) +{ + size_t iEnd = sDir.find_last_not_of( "/\\" ); + if( iEnd == sDir.npos ) + return RString(); + + size_t iStart = sDir.find_last_of( "/\\", iEnd ); + if( iStart == sDir.npos ) + iStart = 0; + else + ++iStart; + + return sDir.substr( iStart, iEnd-iStart+1 ); +} + +/* Return all but the last named component of dir: + * + * a/b/c -> a/b/ + * a/b/c/ -> a/b/ + * c/ -> ./ + * /foo -> / + * / -> / + */ +RString Dirname( const RString &dir ) +{ + // Special case: "/" -> "/". + if( dir.size() == 1 && dir[0] == '/' ) + return "/"; + + int pos = dir.size()-1; + // Skip trailing slashes. + while( pos >= 0 && dir[pos] == '/' ) + --pos; + + // Skip the last component. + while( pos >= 0 && dir[pos] != '/' ) + --pos; + + if( pos < 0 ) + return "./"; + + return dir.substr(0, pos+1); +} + +RString Capitalize( const RString &s ) +{ + if( s.empty() ) + return RString(); + + RString s2 = s; + char *pBuf = s2.GetBuffer(); + UnicodeDoUpper( pBuf, s2.size(), g_UpperCase ); + s2.ReleaseBuffer(); + + return s2; +} + +unsigned char g_UpperCase[256] = +{ + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, + 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F, + 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F, + 0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F, + 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F, + 0x60,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F, + 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x7B,0x7C,0x7D,0x7E,0x7F, + 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8A,0x8B,0x8C,0x8D,0x8E,0x8F, + 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C,0x9D,0x9E,0x9F, + 0xA0,0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xAB,0xAC,0xAD,0xAE,0xAF, + 0xB0,0xB1,0xB2,0xB3,0xB4,0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xBB,0xBC,0xBD,0xBE,0xBF, + 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, + 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xDF, + 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, + 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xF7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xFF, +}; + +unsigned char g_LowerCase[256] = +{ + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, + 0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F, + 0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F, + 0x40,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F, + 0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x5B,0x5C,0x5D,0x5E,0x5F, + 0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F, + 0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x7F, + 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8A,0x8B,0x8C,0x8D,0x8E,0x8F, + 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C,0x9D,0x9E,0x9F, + 0xA0,0xA1,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xAB,0xAC,0xAD,0xAE,0xAF, + 0xB0,0xB1,0xB2,0xB3,0xB4,0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xBB,0xBC,0xBD,0xBE,0xBF, + 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, + 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xDF, + 0xC0,0xC1,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCC,0xCD,0xCE,0xCF, + 0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xF7,0xD8,0xD9,0xDA,0xDB,0xDC,0xDD,0xDE,0xFF, +}; + +void FixSlashesInPlace( RString &sPath ) +{ + for( unsigned i = 0; i < sPath.size(); ++i ) + if( sPath[i] == '\\' ) + sPath[i] = '/'; +} + +/* Keep trailing slashes, since that can be used to illustrate that a path always + * represents a directory. + * + * foo/bar -> foo/bar + * foo/bar/ -> foo/bar/ + * foo///bar/// -> foo/bar/ + * foo/bar/./baz -> foo/bar/baz + * foo/bar/../baz -> foo/baz + * ../foo -> ../foo + * ../../foo -> ../../foo + * ./foo -> foo (if bRemoveLeadingDot), ./foo (if !bRemoveLeadingDot) + * ./ -> ./ + * ./// -> ./ + */ + +void CollapsePath( RString &sPath, bool bRemoveLeadingDot ) +{ + RString sOut; + sOut.reserve( sPath.size() ); + + size_t iPos = 0; + size_t iNext; + for( ; iPos < sPath.size(); iPos = iNext ) + { + // Find the next slash. + iNext = sPath.find( '/', iPos ); + if( iNext == RString::npos ) + iNext = sPath.size(); + else + ++iNext; + + /* Strip extra slashes, but don't remove slashes from the beginning of the string. */ + if( iNext - iPos == 1 && sPath[iPos] == '/' ) + { + if( !sOut.empty() ) + continue; + } + + // If this is a dot, skip it. + if( iNext - iPos == 2 && sPath[iPos] == '.' && sPath[iPos+1] == '/' ) + { + if( bRemoveLeadingDot || !sOut.empty() ) + continue; + } + + // If this is two dots, + if( iNext - iPos == 3 && sPath[iPos] == '.' && sPath[iPos+1] == '.' && sPath[iPos+2] == '/' ) + { + /* If this is the first path element (nothing to delete), + * or all we have is a slash, leave it. */ + if( sOut.empty() || (sOut.size() == 1 && sOut[0] == '/') ) + { + sOut.append( sPath, iPos, iNext-iPos ); + continue; + } + + // Search backwards for the previous path element. + size_t iPrev = sOut.rfind( '/', sOut.size()-2 ); + if( iPrev == RString::npos ) + iPrev = 0; + else + ++iPrev; + + // If the previous element is also .., leave it. + bool bLastIsTwoDots = (sOut.size() - iPrev == 3 && sOut[iPrev] == '.' && sOut[iPrev+1] == '.' ); + if( bLastIsTwoDots ) + { + sOut.append( sPath, iPos, iNext-iPos ); + continue; + } + + sOut.erase( iPrev ); + continue; + } + + sOut.append( sPath, iPos, iNext-iPos ); + } + + sOut.swap( sPath ); +} + +namespace StringConversion +{ + template<> bool FromString( const RString &sValue, int &out ) + { + if( sscanf( sValue.c_str(), "%d", &out ) == 1 ) + return true; + + out = 0; + return false; + } + + template<> bool FromString( const RString &sValue, unsigned &out ) + { + if( sscanf( sValue.c_str(), "%u", &out ) == 1 ) + return true; + + out = 0; + return false; + } + + template<> bool FromString( const RString &sValue, float &out ) + { + const char *endptr = sValue.data() + sValue.size(); + out = strtof( sValue, (char **) &endptr ); + if( endptr != sValue.data() && isfinite( out ) ) + return true; + out = 0; + return false; + } + + template<> bool FromString( const RString &sValue, bool &out ) + { + if( sValue.size() == 0 ) + return false; + + out = (StringToInt(sValue) != 0); + return true; + } + + template<> RString ToString( const int &value ) + { + return ssprintf( "%i", value ); + } + + template<> RString ToString( const unsigned &value ) + { + return ssprintf( "%u", value ); + } + + template<> RString ToString( const float &value ) + { + return ssprintf( "%f", value ); + } + + template<> RString ToString( const bool &value ) + { + return ssprintf( "%i", value ); + } +} + +bool FileCopy( const RString &sSrcFile, const RString &sDstFile ) +{ + if( !sSrcFile.CompareNoCase(sDstFile) ) + { + LOG->Warn( "Tried to copy \"%s\" over itself", sSrcFile.c_str() ); + return false; + } + + RageFile in; + if( !in.Open(sSrcFile, RageFile::READ) ) + return false; + + RageFile out; + if( !out.Open(sDstFile, RageFile::WRITE) ) + return false; + + RString sError; + if( !FileCopy(in, out, sError) ) + { + LOG->Warn( "FileCopy(%s,%s): %s", + sSrcFile.c_str(), sDstFile.c_str(), sError.c_str() ); + return false; + } + + return true; +} + +bool FileCopy( RageFileBasic &in, RageFileBasic &out, RString &sError, bool *bReadError ) +{ + while(1) + { + RString data; + if( in.Read(data, 1024*32) == -1 ) + { + sError = ssprintf( "read error: %s", in.GetError().c_str() ); + if( bReadError != NULL ) + *bReadError = true; + return false; + } + if( data.empty() ) + break; + int i = out.Write(data); + if( i == -1 ) + { + sError = ssprintf( "write error: %s", out.GetError().c_str() ); + if( bReadError != NULL ) + *bReadError = false; + return false; + } + } + + if( out.Flush() == -1 ) + { + sError = ssprintf( "write error: %s", out.GetError().c_str() ); + if( bReadError != NULL ) + *bReadError = false; + return false; + } + + return true; +} + +LuaFunction( SecondsToMSSMsMs, SecondsToMSSMsMs( FArg(1) ) ) +LuaFunction( SecondsToHHMMSS, SecondsToHHMMSS( FArg(1) ) ) +LuaFunction( SecondsToMMSSMsMs, SecondsToMMSSMsMs( FArg(1) ) ) +LuaFunction( SecondsToMMSSMsMsMs, SecondsToMMSSMsMsMs( FArg(1) ) ) +LuaFunction( SecondsToMSS, SecondsToMSS( FArg(1) ) ) +LuaFunction( SecondsToMMSS, SecondsToMMSS( FArg(1) ) ) +LuaFunction( FormatNumberAndSuffix, FormatNumberAndSuffix( IArg(1) ) ) +LuaFunction( Basename, Basename( SArg(1) ) ) +static RString MakeLower( RString s ) { s.MakeLower(); return s; } +LuaFunction( Lowercase, MakeLower( SArg(1) ) ) +static RString MakeUpper( RString s ) { s.MakeUpper(); return s; } +LuaFunction( Uppercase, MakeUpper( SArg(1) ) ) +LuaFunction( mbstrlen, (int)RStringToWstring(SArg(1)).length() ) +LuaFunction( URLEncode, URLEncode( SArg(1) ) ); +LuaFunction( PrettyPercent, PrettyPercent( FArg(1), FArg(2) ) ); +//LuaFunction( IsHexVal, IsHexVal( SArg(1) ) ); +static bool UndocumentedFeature( RString s ){ sm_crash(s); return true; } +LuaFunction( UndocumentedFeature, UndocumentedFeature(SArg(1)) ); + +/* + * Copyright (c) 2001-2005 Chris Danford, Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/ScoreDisplayRave.cpp b/src/ScoreDisplayRave.cpp index 6efb893474..7154a1dc9b 100644 --- a/src/ScoreDisplayRave.cpp +++ b/src/ScoreDisplayRave.cpp @@ -1,91 +1,95 @@ -#include "global.h" -#include "ScoreDisplayRave.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "ThemeManager.h" -#include "ActorUtil.h" -#include "PlayerState.h" - - -ScoreDisplayRave::ScoreDisplayRave() -{ - LOG->Trace( "ScoreDisplayRave::ScoreDisplayRave()" ); - - m_lastLevelSeen = ATTACK_LEVEL_1; - - for( int i=0; iGetPathG("ScoreDisplayRave",ssprintf("stream level%d",i+1)) ); - m_sprMeter[i].SetCropRight( 1.f ); - } - - m_textLevel.LoadFromFont( THEME->GetPathF("ScoreDisplayRave","level") ); - m_textLevel.SetText( "1" ); -} - -void ScoreDisplayRave::Init( const PlayerState* pPlayerState, const PlayerStageStats* pPlayerStageStats ) -{ - ScoreDisplay::Init( pPlayerState, pPlayerStageStats ); - - PlayerNumber pn = pPlayerState->m_PlayerNumber; - - m_sprFrameBase.Load( THEME->GetPathG("ScoreDisplayRave",ssprintf("frame base p%d",pn+1)) ); - this->AddChild( m_sprFrameBase ); - - for( int i=0; iAddChild( &m_sprMeter[i] ); - } - - m_textLevel.SetName( ssprintf("LevelP%d",pn+1) ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_textLevel ); - this->AddChild( &m_textLevel ); - - m_sprFrameOverlay.Load( THEME->GetPathG("ScoreDisplayRave",ssprintf("frame overlay p%d",pn+1)) ); - this->AddChild( m_sprFrameOverlay ); -} - -void ScoreDisplayRave::Update( float fDelta ) -{ - ScoreDisplay::Update( fDelta ); - - float fLevel = m_pPlayerState->m_fSuperMeter; - AttackLevel level = (AttackLevel)(int)fLevel; - - if( level != m_lastLevelSeen ) - { - m_sprMeter[m_lastLevelSeen].SetCropRight( 1.f ); - m_lastLevelSeen = level; - m_textLevel.SetText( ssprintf("%d",level+1) ); - } - - float fPercent = fLevel - level; - m_sprMeter[level].SetCropRight( 1-fPercent ); -} - -/* - * (c) 2003 Chris Danford - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#include "global.h" +#include "ScoreDisplayRave.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "ThemeManager.h" +#include "ActorUtil.h" +#include "PlayerState.h" + + +ScoreDisplayRave::ScoreDisplayRave() +{ + LOG->Trace( "ScoreDisplayRave::ScoreDisplayRave()" ); + + m_lastLevelSeen = ATTACK_LEVEL_1; + + for( int i=0; iGetPathG("ScoreDisplayRave",ssprintf("stream level%d",i+1)) ); + m_sprMeter[i].SetCropRight( 1.f ); + } + + m_textLevel.LoadFromFont( THEME->GetPathF("ScoreDisplayRave","level") ); + m_textLevel.SetText( "1" ); +} + +void ScoreDisplayRave::Init( const PlayerState* pPlayerState, const PlayerStageStats* pPlayerStageStats ) +{ + ScoreDisplay::Init( pPlayerState, pPlayerStageStats ); + + PlayerNumber pn = pPlayerState->m_PlayerNumber; + + m_sprFrameBase.Load( THEME->GetPathG("ScoreDisplayRave",ssprintf("frame base p%d",pn+1)) ); + m_sprFrameBase->SetName( ssprintf("FrameBaseP%d",pn+1) ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_sprFrameBase ); + this->AddChild( m_sprFrameBase ); + + for( int i=0; iAddChild( &m_sprMeter[i] ); + } + + m_textLevel.SetName( ssprintf("LevelP%d",pn+1) ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_textLevel ); + this->AddChild( &m_textLevel ); + + m_sprFrameOverlay.Load( THEME->GetPathG("ScoreDisplayRave",ssprintf("frame overlay p%d",pn+1)) ); + m_sprFrameOverlay->SetName( ssprintf("FrameOverP%d",pn+1) ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_sprFrameOverlay ); + this->AddChild( m_sprFrameOverlay ); +} + +void ScoreDisplayRave::Update( float fDelta ) +{ + ScoreDisplay::Update( fDelta ); + + float fLevel = m_pPlayerState->m_fSuperMeter; + AttackLevel level = (AttackLevel)(int)fLevel; + + if( level != m_lastLevelSeen ) + { + m_sprMeter[m_lastLevelSeen].SetCropRight( 1.f ); + m_lastLevelSeen = level; + m_textLevel.SetText( ssprintf("%d",level+1) ); + } + + float fPercent = fLevel - level; + m_sprMeter[level].SetCropRight( 1-fPercent ); +} + +/* + * (c) 2003 Chris Danford + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/ScreenGameplay.cpp b/src/ScreenGameplay.cpp index 0c26d85123..6d72784288 100644 --- a/src/ScreenGameplay.cpp +++ b/src/ScreenGameplay.cpp @@ -1,2944 +1,2955 @@ -#include "global.h" -#include "ScreenGameplay.h" -#include "SongManager.h" -#include "ScreenManager.h" -#include "GameConstantsAndTypes.h" -#include "PrefsManager.h" -#include "GamePreferences.h" -#include "GameManager.h" -#include "RageFileManager.h" -#include "Steps.h" -#include "RageLog.h" -#include "LifeMeter.h" -#include "GameState.h" -#include "ScoreDisplayNormal.h" -#include "ScoreDisplayPercentage.h" -#include "ScoreDisplayLifeTime.h" -#include "ScoreDisplayOni.h" -#include "ScoreDisplayRave.h" -#include "ThemeManager.h" -#include "RageTimer.h" -#include "ScoreKeeperNormal.h" -#include "ScoreKeeperRave.h" -#include "LyricsLoader.h" -#include "ActorUtil.h" -#include "RageSoundManager.h" -#include "RageSoundReader.h" -#include "RageTextureManager.h" -#include "GameSoundManager.h" -#include "CombinedLifeMeterTug.h" -#include "Inventory.h" -#include "Course.h" -#include "NoteDataUtil.h" -#include "UnlockManager.h" -#include "LightsManager.h" -#include "ProfileManager.h" -#include "StatsManager.h" -#include "PlayerAI.h" // for NUM_SKILL_LEVELS -#include "NetworkSyncManager.h" -#include "Foreach.h" -#include "DancingCharacters.h" -#include "ScreenDimensions.h" -#include "ThemeMetric.h" -#include "PlayerState.h" -#include "Style.h" -#include "LuaManager.h" -#include "MemoryCardManager.h" -#include "CommonMetrics.h" -#include "InputMapper.h" -#include "Game.h" -#include "ActiveAttackList.h" -#include "Player.h" -#include "StepsDisplay.h" -#include "XmlFile.h" -#include "Background.h" -#include "Foreground.h" -#include "ScreenSaveSync.h" -#include "AdjustSync.h" -#include "SongUtil.h" -#include "Song.h" -#include "XmlFileUtil.h" -#include "Profile.h" // for replay data stuff - -// Defines -#define SHOW_LIFE_METER_FOR_DISABLED_PLAYERS THEME->GetMetricB(m_sName,"ShowLifeMeterForDisabledPlayers") -#define SHOW_SCORE_IN_RAVE THEME->GetMetricB(m_sName,"ShowScoreInRave") -#define SONG_POSITION_METER_WIDTH THEME->GetMetricF(m_sName,"SongPositionMeterWidth") -#define PLAYER_X( sName, styleType ) THEME->GetMetricF(m_sName,ssprintf("Player%s%sX",sName.c_str(),StyleTypeToString(styleType).c_str())) -#define STOP_COURSE_EARLY THEME->GetMetricB(m_sName,"StopCourseEarly") // evaluate this every time it's used - -static ThemeMetric INITIAL_BACKGROUND_BRIGHTNESS ("ScreenGameplay","InitialBackgroundBrightness"); -static ThemeMetric SECONDS_BETWEEN_COMMENTS ("ScreenGameplay","SecondsBetweenComments"); -static ThemeMetric SCORE_KEEPER_CLASS ("ScreenGameplay","ScoreKeeperClass"); - -AutoScreenMessage( SM_PlayGo ); - -// received while STATE_DANCING -AutoScreenMessage( SM_LoadNextSong ); -AutoScreenMessage( SM_StartLoadingNextSong ); - -// received while STATE_OUTRO -AutoScreenMessage( SM_DoPrevScreen ); -AutoScreenMessage( SM_DoNextScreen ); - -// received while STATE_INTRO -AutoScreenMessage( SM_StartHereWeGo ); -AutoScreenMessage( SM_StopHereWeGo ); - -AutoScreenMessage( SM_BattleTrickLevel1 ); -AutoScreenMessage( SM_BattleTrickLevel2 ); -AutoScreenMessage( SM_BattleTrickLevel3 ); - -static Preference g_bCenter1Player( "Center1Player", false ); -static Preference g_bShowLyrics( "ShowLyrics", true ); -static Preference g_fNetStartOffset( "NetworkStartOffset", -3.0 ); -static Preference g_bEasterEggs( "EasterEggs", true ); - - -PlayerInfo::PlayerInfo(): m_pn(PLAYER_INVALID), m_mp(MultiPlayer_Invalid), - m_bIsDummy(false), m_iDummyIndex(0), m_iAddToDifficulty(0), - m_bPlayerEnabled(false), m_PlayerStateDummy(), - m_PlayerStageStatsDummy(), m_SoundEffectControl(), - m_vpStepsQueue(), m_asModifiersQueue(), m_pLifeMeter(NULL), - m_ptextCourseSongNumber(NULL), m_ptextStepsDescription(NULL), - m_pPrimaryScoreDisplay(NULL), m_pSecondaryScoreDisplay(NULL), - m_pPrimaryScoreKeeper(NULL), m_pSecondaryScoreKeeper(NULL), - m_ptextPlayerOptions(NULL), m_pActiveAttackList(NULL), - m_NoteData(), m_pPlayer(NULL), m_pInventory(NULL), - m_pStepsDisplay(NULL), m_sprOniGameOver() {} - -void PlayerInfo::Load( PlayerNumber pn, MultiPlayer mp, bool bShowNoteField, int iAddToDifficulty ) -{ - m_pn = pn; - m_mp = mp; - m_bPlayerEnabled = IsEnabled(); - m_bIsDummy = false; - m_iAddToDifficulty = iAddToDifficulty; - m_pLifeMeter = NULL; - m_ptextCourseSongNumber = NULL; - m_ptextStepsDescription = NULL; - - if( !IsMultiPlayer() ) - { - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_REGULAR: - case PLAY_MODE_NONSTOP: - case PLAY_MODE_BATTLE: - case PLAY_MODE_RAVE: - if( PREFSMAN->m_bPercentageScoring ) - m_pPrimaryScoreDisplay = new ScoreDisplayPercentage; - else - m_pPrimaryScoreDisplay = new ScoreDisplayNormal; - break; - case PLAY_MODE_ONI: - case PLAY_MODE_ENDLESS: - if( GAMESTATE->m_SongOptions.GetStage().m_LifeType == SongOptions::LIFE_TIME ) - m_pPrimaryScoreDisplay = new ScoreDisplayLifeTime; - else - m_pPrimaryScoreDisplay = new ScoreDisplayOni; - break; - default: - ASSERT(0); - } - } - - PlayerState *const pPlayerState = GetPlayerState(); - PlayerStageStats *const pPlayerStageStats = GetPlayerStageStats(); - - if( m_pPrimaryScoreDisplay ) - m_pPrimaryScoreDisplay->Init( pPlayerState, pPlayerStageStats ); - - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_RAVE: - m_pSecondaryScoreDisplay = new ScoreDisplayRave; - m_pSecondaryScoreDisplay->SetName( "ScoreDisplayRave" ); - default: - break; - } - - if( m_pSecondaryScoreDisplay ) - m_pSecondaryScoreDisplay->Init( pPlayerState, pPlayerStageStats ); - - m_pPrimaryScoreKeeper = ScoreKeeper::MakeScoreKeeper( SCORE_KEEPER_CLASS, pPlayerState, pPlayerStageStats ); - - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_RAVE: - m_pSecondaryScoreKeeper = new ScoreKeeperRave( pPlayerState, pPlayerStageStats ); - default: - break; - } - - m_ptextPlayerOptions = NULL; - m_pActiveAttackList = NULL; - m_pPlayer = new Player( m_NoteData, bShowNoteField ); - m_pInventory = NULL; - m_pStepsDisplay = NULL; - - if( IsMultiPlayer() ) - { - pPlayerState->m_PlayerOptions = GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerOptions; - } -} - -void PlayerInfo::LoadDummyP1( int iDummyIndex, int iAddToDifficulty ) -{ - m_pn = PLAYER_1; - m_bPlayerEnabled = IsEnabled(); - m_bIsDummy = true; - m_iDummyIndex = iDummyIndex; - m_iAddToDifficulty = iAddToDifficulty; - - // don't init any of the scoring objects - m_pPlayer = new Player( m_NoteData, true ); - - // PlayerOptions needs to be set now so that we load the correct NoteSkin. - m_PlayerStateDummy = *GAMESTATE->m_pPlayerState[PLAYER_1]; -} - -PlayerInfo::~PlayerInfo() -{ - SAFE_DELETE( m_pLifeMeter ); - SAFE_DELETE( m_ptextCourseSongNumber ); - SAFE_DELETE( m_ptextStepsDescription ); - SAFE_DELETE( m_pPrimaryScoreDisplay ); - SAFE_DELETE( m_pSecondaryScoreDisplay ); - SAFE_DELETE( m_pPrimaryScoreKeeper ); - SAFE_DELETE( m_pSecondaryScoreKeeper ); - SAFE_DELETE( m_ptextPlayerOptions ); - SAFE_DELETE( m_pActiveAttackList ); - SAFE_DELETE( m_pPlayer ); - SAFE_DELETE( m_pInventory ); - SAFE_DELETE( m_pStepsDisplay ); -} - -void PlayerInfo::ShowOniGameOver() -{ - m_sprOniGameOver->PlayCommand( "Die" ); -} - -PlayerState *PlayerInfo::GetPlayerState() -{ - if( m_bIsDummy ) - return &m_PlayerStateDummy; - return IsMultiPlayer() ? - GAMESTATE->m_pMultiPlayerState[ GetPlayerStateAndStageStatsIndex() ] : - GAMESTATE->m_pPlayerState[ GetPlayerStateAndStageStatsIndex() ]; -} - -PlayerStageStats *PlayerInfo::GetPlayerStageStats() -{ - // multiplayer chooses the PlayerStageStats with the highest score on StageFinalized - if( m_bIsDummy || IsMultiPlayer() ) - return &m_PlayerStageStatsDummy; - return &STATSMAN->m_CurStageStats.m_player[ GetPlayerStateAndStageStatsIndex() ]; -} - -bool PlayerInfo::IsEnabled() -{ - if( m_pn != PLAYER_INVALID ) - return GAMESTATE->IsPlayerEnabled( m_pn ); - if( m_mp != MultiPlayer_Invalid ) - return GAMESTATE->IsMultiPlayerEnabled( m_mp ); - else if( m_bIsDummy ) - return true; - ASSERT( 0 ); - return false; -} - -vector::iterator -GetNextEnabledPlayerInfo( vector::iterator iter, vector &v ) -{ - for( ; iter != v.end(); ++iter ) - { - if( !iter->m_bPlayerEnabled ) - continue; - return iter; - } - return iter; -} - -vector::iterator -GetNextEnabledPlayerInfoNotDummy( vector::iterator iter, vector &v ) -{ - for( ; iter != v.end(); iter++ ) - { - if( iter->m_bIsDummy ) - continue; - if( !iter->m_bPlayerEnabled ) - continue; - return iter; - } - return iter; -} - -vector::iterator -GetNextEnabledPlayerNumberInfo( vector::iterator iter, vector &v ) -{ - for( ; iter != v.end(); ++iter ) - { - if( iter->m_bIsDummy ) - continue; - if( !iter->m_bPlayerEnabled ) - continue; - if( iter->m_mp != MultiPlayer_Invalid ) - continue; - return iter; - } - return iter; -} - -vector::iterator -GetNextPlayerNumberInfo( vector::iterator iter, vector &v ) -{ - for( ; iter != v.end(); ++iter ) - { - if( iter->m_bIsDummy ) - continue; - if( iter->m_pn == PLAYER_INVALID ) - continue; - return iter; - } - return iter; -} - -vector::iterator -GetNextVisiblePlayerInfo( vector::iterator iter, vector &v ) -{ - for( ; iter != v.end(); ++iter ) - { - if( !iter->m_pPlayer->HasVisibleParts() ) - continue; - return iter; - } - return iter; -} - -//////////////////////////////////////////////////////////////////////////////// - -ScreenGameplay::ScreenGameplay() -{ - m_pSongBackground = NULL; - m_pSongForeground = NULL; -} - -void ScreenGameplay::Init() -{ - SubscribeToMessage( "Judgment" ); - - PLAYER_TYPE.Load( m_sName, "PlayerType" ); - PLAYER_INIT_COMMAND.Load( m_sName, "PlayerInitCommand" ); - GIVE_UP_START_TEXT.Load( m_sName, "GiveUpStartText" ); - GIVE_UP_BACK_TEXT.Load( m_sName, "GiveUpBackText" ); - GIVE_UP_ABORTED_TEXT.Load( m_sName, "GiveUpAbortedText" ); - MUSIC_FADE_OUT_SECONDS.Load( m_sName, "MusicFadeOutSeconds" ); - OUT_TRANSITION_LENGTH.Load( m_sName, "OutTransitionLength" ); - COURSE_TRANSITION_LENGTH.Load( m_sName, "CourseTransitionLength" ); - BEGIN_FAILED_DELAY.Load( m_sName, "BeginFailedDelay" ); - MIN_SECONDS_TO_STEP.Load( m_sName, "MinSecondsToStep" ); - MIN_SECONDS_TO_MUSIC.Load( m_sName, "MinSecondsToMusic" ); - MIN_SECONDS_TO_STEP_NEXT_SONG.Load( m_sName, "MinSecondsToStepNextSong" ); - START_GIVES_UP.Load( m_sName, "StartGivesUp" ); - BACK_GIVES_UP.Load( m_sName, "BackGivesUp" ); - GIVING_UP_GOES_TO_PREV_SCREEN.Load( m_sName, "GivingUpGoesToPrevScreen" ); - FAIL_ON_MISS_COMBO.Load( m_sName, "FailOnMissCombo" ); - ALLOW_CENTER_1_PLAYER.Load( m_sName, "AllowCenter1Player" ); - // configurable: - UNPAUSE_WITH_START.Load( m_sName, "UnpauseWithStart"); - SURVIVAL_MOD_OVERRIDE.Load(m_sName, "SurvivalModOverride"); - - if( UseSongBackgroundAndForeground() ) - { - m_pSongBackground = new Background; - m_pSongForeground = new Foreground; - } - - ScreenWithMenuElements::Init(); - - this->FillPlayerInfo( m_vPlayerInfo ); - - { - ASSERT_M( !m_vPlayerInfo.empty(), "m_vPlayerInfo must be filled by FillPlayerInfo" ); - - int iNumEnabledPlayers = 0; - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - ++iNumEnabledPlayers; - - /* If this is 0, we have no active players and havn't been initialized correctly. */ - ASSERT( iNumEnabledPlayers > 0 ); - } - - /* Pause MEMCARDMAN. If a memory card is removed, we don't want to interrupt the - * player by making a noise until the game finishes. */ - if( !GAMESTATE->m_bDemonstrationOrJukebox ) - MEMCARDMAN->PauseMountingThread(); - - m_pSoundMusic = NULL; - m_bPaused = false; - - m_pCombinedLifeMeter = NULL; - - if( GAMESTATE->m_pCurSong == NULL && GAMESTATE->m_pCurCourse == NULL ) - return; // ScreenDemonstration will move us to the next screen. We just need to survive for one update without crashing. - - /* Save settings to the profile now. Don't do this on extra stages, since the - * user doesn't have full control; saving would force profiles to Difficulty_Hard - * and save over their default modifiers every time someone got an extra stage. - * Do this before course modifiers are set up. */ - if( !GAMESTATE->IsAnExtraStage() ) - { - FOREACH_HumanPlayer( pn ) - GAMESTATE->SaveCurrentSettingsToProfile(pn); - } - - /* Called once per stage (single song or single course). */ - GAMESTATE->BeginStage(); - - int player = 1; - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - unsigned int count = pi->m_vpStepsQueue.size(); - - for (unsigned int i = 0; i < count; i++) - { - Steps *curSteps = pi->m_vpStepsQueue[i]; - if (curSteps->IsNoteDataEmpty()) - { - if (curSteps->GetNoteDataFromSimfile()) - { - LOG->Trace("Notes should be loaded for player %d", player); - } - else - { - LOG->Trace("Error loading notes for player %d", player); - } - } - } - player++; - } - - if(!GAMESTATE->IsCourseMode() && !GAMESTATE->m_bDemonstrationOrJukebox) - { - // fill in difficulty of CPU players with that of the first human player - // this should not need to worry about step content. - FOREACH_PotentialCpuPlayer(p) - GAMESTATE->m_pCurSteps[p].Set( GAMESTATE->m_pCurSteps[ GAMESTATE->GetFirstHumanPlayer() ] ); - - FOREACH_EnabledPlayer(p) - ASSERT( GAMESTATE->m_pCurSteps[p].Get() ); - } - - /* Increment the course play count. */ - if( GAMESTATE->IsCourseMode() && !GAMESTATE->m_bDemonstrationOrJukebox ) - FOREACH_EnabledPlayer(p) - PROFILEMAN->IncrementCoursePlayCount( GAMESTATE->m_pCurCourse, GAMESTATE->m_pCurTrail[p], p ); - - STATSMAN->m_CurStageStats.m_Stage = GAMESTATE->GetCurrentStage(); - STATSMAN->m_CurStageStats.m_iStageIndex = GAMESTATE->m_iCurrentStageIndex; - STATSMAN->m_CurStageStats.m_playMode = GAMESTATE->m_PlayMode; - STATSMAN->m_CurStageStats.m_pStyle = GAMESTATE->GetCurrentStyle(); - - /* Record combo rollover. */ - FOREACH_EnabledPlayerInfoNotDummy( m_vPlayerInfo, pi ) - pi->GetPlayerStageStats()->UpdateComboList( 0, true ); - - m_DancingState = STATE_INTRO; - - // Set this in LoadNextSong() - //m_fTimeLeftBeforeDancingComment = SECONDS_BETWEEN_COMMENTS; - - m_bZeroDeltaOnNextUpdate = false; - - - if( m_pSongBackground ) - { - m_pSongBackground->SetName( "SongBackground" ); - m_pSongBackground->SetDrawOrder( DRAW_ORDER_BEFORE_EVERYTHING ); - ActorUtil::LoadAllCommands( *m_pSongBackground, m_sName ); - this->AddChild( m_pSongBackground ); - } - - if( m_pSongForeground ) - { - m_pSongForeground->SetName( "SongForeground" ); - m_pSongForeground->SetDrawOrder( DRAW_ORDER_OVERLAY+1 ); // on top of the overlay, but under transitions - ActorUtil::LoadAllCommands( *m_pSongForeground, m_sName ); - this->AddChild( m_pSongForeground ); - } - - if( PREFSMAN->m_bShowBeginnerHelper ) - { - m_BeginnerHelper.SetDrawOrder( DRAW_ORDER_BEFORE_EVERYTHING ); - m_BeginnerHelper.SetXY( SCREEN_CENTER_X, SCREEN_CENTER_Y ); - this->AddChild( &m_BeginnerHelper ); - } - - if( !GAMESTATE->m_bDemonstrationOrJukebox ) // only load if we're going to use it - { - m_Toasty.Load( THEME->GetPathB(m_sName,"toasty") ); - this->AddChild( &m_Toasty ); - } - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - RString sName = ssprintf("Player%s", pi->GetName().c_str()); - pi->m_pPlayer->SetName( sName ); - // If pi->m_pn is set, then the player will be visible. If not, then it's not - // visible and don't bother setting its position. - - float fPlayerX; - if( GAMESTATE->m_bMultiplayer && !pi->m_bIsDummy ) - { - fPlayerX = SCREEN_CENTER_X; - } - else - { - fPlayerX = PLAYER_X( pi->GetName(), GAMESTATE->GetCurrentStyle()->m_StyleType ); - if( Center1Player() ) - fPlayerX = SCREEN_CENTER_X; - } - - pi->m_pPlayer->SetX( fPlayerX ); - pi->m_pPlayer->RunCommands( PLAYER_INIT_COMMAND ); - //ActorUtil::LoadAllCommands(pi->m_pPlayer, m_sName); - this->AddChild( pi->m_pPlayer ); - pi->m_pPlayer->PlayCommand( "On" ); - } - - FOREACH_EnabledPlayerInfoNotDummy( m_vPlayerInfo, pi ) - { - if( pi->m_pPlayer->HasVisibleParts() ) - { - pi->m_sprOniGameOver.Load( THEME->GetPathG(m_sName,"oni gameover") ); - pi->m_sprOniGameOver->SetName( ssprintf("OniGameOver%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_sprOniGameOver ); - this->AddChild( pi->m_sprOniGameOver ); - } - } - - m_NextSong.Load( THEME->GetPathB(m_sName,"next course song") ); - m_NextSong.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); - this->AddChild( &m_NextSong ); - - // Add combined life meter - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_BATTLE: - case PLAY_MODE_RAVE: - m_pCombinedLifeMeter = new CombinedLifeMeterTug; - m_pCombinedLifeMeter->SetName( "CombinedLife" ); - LOAD_ALL_COMMANDS_AND_SET_XY( *m_pCombinedLifeMeter ); - this->AddChild( m_pCombinedLifeMeter ); - default: - break; - } - - // Before the lifemeter loads, if Networking is required - // we need to wait, so that there is no Dead On Start issues. - // if you wait too long at the second checkpoint, you will - // appear dead when you begin your game. - NSMAN->StartRequest(0); - - - // Add individual life meter - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_REGULAR: - case PLAY_MODE_ONI: - case PLAY_MODE_NONSTOP: - case PLAY_MODE_ENDLESS: - FOREACH_PlayerNumberInfo( m_vPlayerInfo, pi ) - { - if( !GAMESTATE->IsPlayerEnabled(pi->m_pn) && !SHOW_LIFE_METER_FOR_DISABLED_PLAYERS ) - continue; // skip - - pi->m_pLifeMeter = LifeMeter::MakeLifeMeter( GAMESTATE->m_SongOptions.GetStage().m_LifeType ); - pi->m_pLifeMeter->Load( pi->GetPlayerState(), pi->GetPlayerStageStats() ); - pi->m_pLifeMeter->SetName( ssprintf("Life%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pLifeMeter ); - this->AddChild( pi->m_pLifeMeter ); - } - break; - case PLAY_MODE_BATTLE: - case PLAY_MODE_RAVE: - default: - break; - } - - m_bShowScoreboard = false; - -#if !defined(WITHOUT_NETWORKING) - // Only used in SMLAN/SMOnline: - if( NSMAN->useSMserver && GAMESTATE->GetCurrentStyle()->m_StyleType != StyleType_OnePlayerTwoSides ) - { - m_bShowScoreboard = PREFSMAN->m_bEnableScoreboard.Get(); - PlayerNumber pn = GAMESTATE->GetFirstDisabledPlayer(); - if( pn != PLAYER_INVALID && m_bShowScoreboard ) - { - FOREACH_NSScoreBoardColumn( col ) - { - m_Scoreboard[col].LoadFromFont( THEME->GetPathF(m_sName,"scoreboard") ); - m_Scoreboard[col].SetShadowLength( 0 ); - m_Scoreboard[col].SetName( ssprintf("ScoreboardC%iP%i",col+1,pn+1) ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_Scoreboard[col] ); - m_Scoreboard[col].SetText( NSMAN->m_Scoreboard[col] ); - m_Scoreboard[col].SetVertAlign( align_top ); - this->AddChild( &m_Scoreboard[col] ); - } - } - } -#endif - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - // primary score display - if( pi->m_pPrimaryScoreDisplay ) - { - pi->m_pPrimaryScoreDisplay->SetName( ssprintf("Score%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pPrimaryScoreDisplay ); - if( GAMESTATE->m_PlayMode != PLAY_MODE_RAVE || SHOW_SCORE_IN_RAVE ) /* XXX: ugly */ - this->AddChild( pi->m_pPrimaryScoreDisplay ); - } - - // secondary score display - if( pi->m_pSecondaryScoreDisplay ) - { - pi->m_pSecondaryScoreDisplay->SetName( ssprintf("SecondaryScore%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pSecondaryScoreDisplay ); - this->AddChild( pi->m_pSecondaryScoreDisplay ); - } - } - - // Add stage / SongNumber - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - if( GAMESTATE->IsCourseMode() ) - { - ASSERT( pi->m_ptextCourseSongNumber == NULL ); - SONG_NUMBER_FORMAT.Load( m_sName, "SongNumberFormat" ); - pi->m_ptextCourseSongNumber = new BitmapText; - pi->m_ptextCourseSongNumber->LoadFromFont( THEME->GetPathF(m_sName,"SongNum") ); - pi->m_ptextCourseSongNumber->SetName( ssprintf("SongNumber%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_ptextCourseSongNumber ); - pi->m_ptextCourseSongNumber->SetText( "" ); - //pi->m_ptextCourseSongNumber->SetDiffuse( RageColor(0,0.5f,1,1) ); // light blue - this->AddChild( pi->m_ptextCourseSongNumber ); - } - - ASSERT( pi->m_ptextStepsDescription == NULL ); - pi->m_ptextStepsDescription = new BitmapText; - pi->m_ptextStepsDescription->LoadFromFont( THEME->GetPathF(m_sName,"StepsDescription") ); - pi->m_ptextStepsDescription->SetName( ssprintf("StepsDescription%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_ptextStepsDescription ); - this->AddChild( pi->m_ptextStepsDescription ); - - // Player/Song options - ASSERT( pi->m_ptextPlayerOptions == NULL ); - pi->m_ptextPlayerOptions = new BitmapText; - pi->m_ptextPlayerOptions->LoadFromFont( THEME->GetPathF(m_sName,"player options") ); - pi->m_ptextPlayerOptions->SetName( ssprintf("PlayerOptions%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_ptextPlayerOptions ); - this->AddChild( pi->m_ptextPlayerOptions ); - - // Difficulty icon and meter - ASSERT( pi->m_pStepsDisplay == NULL ); - pi->m_pStepsDisplay = new StepsDisplay; - pi->m_pStepsDisplay->Load("StepsDisplayGameplay", pi->GetPlayerState() ); - pi->m_pStepsDisplay->SetName( ssprintf("StepsDisplay%s",pi->GetName().c_str()) ); - PlayerNumber pn = pi->GetStepsAndTrailIndex(); - if( pn != PlayerNumber_Invalid ) - pi->m_pStepsDisplay->PlayCommand( "Set" + pi->GetName() ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pStepsDisplay ); - this->AddChild( pi->m_pStepsDisplay ); - -/* - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_BATTLE: - pi->m_pInventory = new Inventory; - pi->m_pInventory->Load( p ); - this->AddChild( pi->m_pInventory ); - break; - } -*/ - } - - m_textSongOptions.LoadFromFont( THEME->GetPathF(m_sName,"song options") ); - m_textSongOptions.SetShadowLength( 0 ); - m_textSongOptions.SetName( "SongOptions" ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_textSongOptions ); - m_textSongOptions.SetText( GAMESTATE->m_SongOptions.GetStage().GetLocalizedString() ); - this->AddChild( &m_textSongOptions ); - - FOREACH_VisiblePlayerInfo( m_vPlayerInfo, pi ) - { - ASSERT( pi->m_pActiveAttackList == NULL ); - pi->m_pActiveAttackList = new ActiveAttackList; - pi->m_pActiveAttackList->LoadFromFont( THEME->GetPathF(m_sName,"ActiveAttackList") ); - pi->m_pActiveAttackList->Init( pi->GetPlayerState() ); - pi->m_pActiveAttackList->SetName( ssprintf("ActiveAttackList%s",pi->GetName().c_str()) ); - LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pActiveAttackList ); - this->AddChild( pi->m_pActiveAttackList ); - } - - if( g_bShowLyrics ) - { - m_LyricDisplay.SetName( "LyricDisplay" ); - LOAD_ALL_COMMANDS( m_LyricDisplay ); - this->AddChild( &m_LyricDisplay ); - } - - if( !GAMESTATE->m_bDemonstrationOrJukebox ) // only load if we're going to use it - { - m_Ready.Load( THEME->GetPathB(m_sName,"ready") ); - this->AddChild( &m_Ready ); - - m_Go.Load( THEME->GetPathB(m_sName,"go") ); - this->AddChild( &m_Go ); - - m_Failed.Load( THEME->GetPathB(m_sName,"failed") ); - m_Failed.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); // on top of everything else - this->AddChild( &m_Failed ); - - m_textDebug.LoadFromFont( THEME->GetPathF(m_sName,"debug") ); - m_textDebug.SetName( "Debug" ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_textDebug ); - m_textDebug.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); // just under transitions, over the foreground - this->AddChild( &m_textDebug ); - - m_GameplayAssist.Init(); - - if( GAMESTATE->IsAnExtraStage() ) // only load if we're going to use it - { - m_textSurviveTime.LoadFromFont( THEME->GetPathF(m_sName,"survive time") ); - m_textSurviveTime.SetShadowLength( 0 ); - m_textSurviveTime.SetName( "SurviveTime" ); - LOAD_ALL_COMMANDS_AND_SET_XY( m_textSurviveTime ); - m_textSurviveTime.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); - m_textSurviveTime.SetDiffuse( RageColor(1,1,1,0) ); - this->AddChild( &m_textSurviveTime ); - } - - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_BATTLE: - m_soundBattleTrickLevel1.Load( THEME->GetPathS(m_sName,"battle trick level1"), true ); - m_soundBattleTrickLevel2.Load( THEME->GetPathS(m_sName,"battle trick level2"), true ); - m_soundBattleTrickLevel3.Load( THEME->GetPathS(m_sName,"battle trick level3"), true ); - default: - break; - } - } - - if( m_pSongBackground ) - m_pSongBackground->Init(); - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - RString sType = PLAYER_TYPE; - if( pi->m_bIsDummy ) - sType += "Dummy"; - pi->m_pPlayer->Init( - sType, - pi->GetPlayerState(), - pi->GetPlayerStageStats(), - pi->m_pLifeMeter, - m_pCombinedLifeMeter, - pi->m_pPrimaryScoreDisplay, - pi->m_pSecondaryScoreDisplay, - pi->m_pInventory, - pi->m_pPrimaryScoreKeeper, - pi->m_pSecondaryScoreKeeper ); - } - - // fill in m_apSongsQueue, m_vpStepsQueue, m_asModifiersQueue - InitSongQueues(); - - // Fill StageStats - STATSMAN->m_CurStageStats.m_vpPossibleSongs = m_apSongsQueue; - FOREACH( PlayerInfo, m_vPlayerInfo, pi ) - { - if( pi->GetPlayerStageStats() ) - pi->GetPlayerStageStats()->m_vpPossibleSteps = pi->m_vpStepsQueue; - } - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - ASSERT( !pi->m_vpStepsQueue.empty() ); - if( pi->GetPlayerStageStats() ) - pi->GetPlayerStageStats()->m_bJoined = true; - if( pi->m_pPrimaryScoreKeeper ) - pi->m_pPrimaryScoreKeeper->Load( m_apSongsQueue, pi->m_vpStepsQueue, pi->m_asModifiersQueue ); - if( pi->m_pSecondaryScoreKeeper ) - pi->m_pSecondaryScoreKeeper->Load( m_apSongsQueue, pi->m_vpStepsQueue, pi->m_asModifiersQueue ); - } - - GAMESTATE->m_bGameplayLeadIn.Set( true ); - - /* LoadNextSong first, since that positions some elements which need to be - * positioned before we TweenOnScreen. */ - LoadNextSong(); - - m_GiveUpTimer.SetZero(); -} - -bool ScreenGameplay::Center1Player() const -{ - /* Perhaps this should be handled better by defining a new - * StyleType for ONE_PLAYER_ONE_CREDIT_AND_ONE_COMPUTER, - * but for now just ignore Center1Player when it's Battle or Rave - * Mode. This doesn't begin to address two-player solo (6 arrows) */ - return g_bCenter1Player && - (bool)ALLOW_CENTER_1_PLAYER && - GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && - GAMESTATE->m_PlayMode != PLAY_MODE_RAVE && - GAMESTATE->GetCurrentStyle()->m_StyleType == StyleType_OnePlayerOneSide; -} - -// fill in m_apSongsQueue, m_vpStepsQueue, m_asModifiersQueue -void ScreenGameplay::InitSongQueues() -{ - if( GAMESTATE->IsCourseMode() ) - { - Course* pCourse = GAMESTATE->m_pCurCourse; - ASSERT( pCourse ); - - m_apSongsQueue.clear(); - PlayerNumber pnMaster = GAMESTATE->GetMasterPlayerNumber(); - Trail *pTrail = GAMESTATE->m_pCurTrail[pnMaster]; - ASSERT( pTrail ); - FOREACH_CONST( TrailEntry, pTrail->m_vEntries, e ) - { - ASSERT( e->pSong ); - m_apSongsQueue.push_back( e->pSong ); - } - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - Trail *lTrail = GAMESTATE->m_pCurTrail[ pi->GetStepsAndTrailIndex() ]; - ASSERT( lTrail ); - - pi->m_vpStepsQueue.clear(); - pi->m_asModifiersQueue.clear(); - FOREACH_CONST( TrailEntry, lTrail->m_vEntries, e ) - { - ASSERT( e->pSteps ); - pi->m_vpStepsQueue.push_back( e->pSteps ); - AttackArray a; - e->GetAttackArray( a ); - pi->m_asModifiersQueue.push_back( a ); - } - - // In a survival course, override stored mods - if( pCourse->GetCourseType() == COURSE_TYPE_SURVIVAL && SURVIVAL_MOD_OVERRIDE ) - { - pi->GetPlayerState()->m_PlayerOptions.FromString( ModsLevel_Stage, - "clearall," - + CommonMetrics::DEFAULT_NOTESKIN_NAME.GetValue() - + "," - + CommonMetrics::DEFAULT_MODIFIERS.GetValue() ); - pi->GetPlayerState()->RebuildPlayerOptionsFromActiveAttacks(); - } - } - } - else - { - m_apSongsQueue.push_back( GAMESTATE->m_pCurSong ); - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - Steps *pSteps = GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ]; - pi->m_vpStepsQueue.push_back( pSteps ); - const PlayerOptions &p = pi->GetPlayerState()->m_PlayerOptions.GetCurrent(); - - if (p.m_fNoAttack == 0 && p.m_fRandAttack == 0 && - pSteps->m_Attacks.size() > 0 ) - { - pi->m_asModifiersQueue.push_back( pSteps->m_Attacks ); - } - else - { - AttackArray aa; - pi->m_asModifiersQueue.push_back( aa ); - } - } - } - - if( GAMESTATE->m_bMultiplayer ) - { - for( int i=0; i<(int)m_apSongsQueue.size(); i++ ) - { - Song *pSong = m_apSongsQueue[i]; - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - Steps *pOldSteps = pi->m_vpStepsQueue[i]; - - vector vpSteps; - SongUtil::GetSteps( pSong, vpSteps, pOldSteps->m_StepsType ); - StepsUtil::SortNotesArrayByDifficulty( vpSteps ); - vector::iterator iter = find( vpSteps.begin(), vpSteps.end(), pOldSteps ); - int iIndexBase = 0; - if( iter != vpSteps.end() ) - { - iIndexBase = iter - vpSteps.begin(); - CLAMP( iIndexBase, 0, vpSteps.size() - GAMESTATE->m_iNumMultiplayerNoteFields ); - } - - int iIndexToUse = iIndexBase + pi->m_iAddToDifficulty; - CLAMP( iIndexToUse, 0, vpSteps.size()-1 ); - - Steps *pSteps = vpSteps[iIndexToUse]; - pi->m_vpStepsQueue[i] = pSteps; - } - } - } -} - -ScreenGameplay::~ScreenGameplay() -{ - if( this->IsFirstUpdate() ) - { - /* We never received any updates. That means we were deleted without being - * used, and never actually played. (This can happen when backing out of - * ScreenStage.) Cancel the stage. */ - GAMESTATE->CancelStage(); - } - - LOG->Trace( "ScreenGameplay::~ScreenGameplay()" ); - - SAFE_DELETE( m_pSongBackground ); - SAFE_DELETE( m_pSongForeground ); - - if( !GAMESTATE->m_bDemonstrationOrJukebox ) - MEMCARDMAN->UnPauseMountingThread(); - - SAFE_DELETE( m_pCombinedLifeMeter ); - if( m_pSoundMusic ) - m_pSoundMusic->StopPlaying(); - - m_GameplayAssist.StopPlaying(); - - NSMAN->ReportSongOver(); -} - -bool ScreenGameplay::IsLastSong() -{ - if( GAMESTATE->m_pCurCourse ) - { - if( GAMESTATE->m_pCurCourse->m_fGoalSeconds > 0 && GAMESTATE->m_bWorkoutGoalComplete ) - return true; - } - if( GAMESTATE->m_pCurCourse && GAMESTATE->m_pCurCourse->m_bRepeat ) - return false; - return GAMESTATE->GetCourseSongIndex() >= (int)m_apSongsQueue.size()-1; // GetCourseSongIndex() is 0-based -} - -void ScreenGameplay::SetupSong( int iSongIndex ) -{ - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - /* This is the first beat that can be changed without it being visible. - * Until we draw for the first time, any beat can be changed. */ - pi->GetPlayerState()->m_fLastDrawnBeat = -100; - - Steps *pSteps = pi->m_vpStepsQueue[iSongIndex]; - GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ].Set( pSteps ); - - /* Load new NoteData into Player. Do this before - * RebuildPlayerOptionsFromActiveAttacks or else transform mods will get - * propagated to GAMESTATE->m_pPlayerOptions too early and be double-applied - * to the NoteData: - * once in Player::Load, then again in Player::ApplyActiveAttacks. - * This is very bad for transforms like AddMines. - */ - NoteData originalNoteData; - pSteps->GetNoteData( originalNoteData ); - - const Style* pStyle = GAMESTATE->GetCurrentStyle(); - NoteData ndTransformed; - pStyle->GetTransformedNoteDataForStyle( pi->GetStepsAndTrailIndex(), originalNoteData, ndTransformed ); - - // HACK: Apply NoteSkins from global course options. Do this before - // Player::Load, since it needs to know which note skin to load. - pi->GetPlayerState()->m_ModsToApply.clear(); - for( unsigned i=0; im_asModifiersQueue[iSongIndex].size(); ++i ) - { - Attack a = pi->m_asModifiersQueue[iSongIndex][i]; - if( a.fStartSecond != 0 ) - continue; - a.fStartSecond = a.ATTACK_STARTS_NOW; // now - - PlayerOptions po; - po.FromString( a.sModifiers ); - if( po.m_sNoteSkin.empty() ) - continue; - a.sModifiers = po.m_sNoteSkin; - - pi->GetPlayerState()->LaunchAttack( a ); - } - - /* Update attack bOn flags, and rebuild Current-level options - * from Song-level options. The current NoteSkin could have changed - * because of an attack ending. */ - pi->GetPlayerState()->Update( 0 ); - - // load player - { - pi->m_NoteData = ndTransformed; - NoteDataUtil::RemoveAllTapsOfType( pi->m_NoteData, TapNote::autoKeysound ); - pi->m_pPlayer->Load(); - } - - // load auto keysounds - { - NoteData nd = ndTransformed; - NoteDataUtil::RemoveAllTapsExceptForType( nd, TapNote::autoKeysound ); - m_AutoKeysounds.Load( pi->GetStepsAndTrailIndex(), nd ); - } - - { - RString sType; - switch( GAMESTATE->m_SongOptions.GetCurrent().m_SoundEffectType ) - { - case SongOptions::SOUNDEFFECT_OFF: sType = "SoundEffectControl_Off"; break; - case SongOptions::SOUNDEFFECT_SPEED: sType = "SoundEffectControl_Speed"; break; - case SongOptions::SOUNDEFFECT_PITCH: sType = "SoundEffectControl_Pitch"; break; - default: break; - } - - pi->m_SoundEffectControl.Load( sType, pi->GetPlayerState(), &pi->m_NoteData ); - } - - // Put course options into effect. Do this after Player::Load so - // that mods aren't double-applied. - pi->GetPlayerState()->m_ModsToApply.clear(); - for( unsigned i=0; im_asModifiersQueue[iSongIndex].size(); ++i ) - { - Attack a = pi->m_asModifiersQueue[iSongIndex][i]; - if( a.fStartSecond == 0 ) - a.fStartSecond = a.ATTACK_STARTS_NOW; // now - - pi->GetPlayerState()->LaunchAttack( a ); - GAMESTATE->m_SongOptions.FromString( ModsLevel_Song, a.sModifiers ); - } - - // Update attack bOn flags. - pi->GetPlayerState()->Update( 0 ); - - // Hack: Course modifiers that are set to start immediately shouldn't tween on. - pi->GetPlayerState()->m_PlayerOptions.SetCurrentToLevel( ModsLevel_Stage ); - } -} - -void ScreenGameplay::ReloadCurrentSong() -{ - FOREACH_EnabledPlayerInfoNotDummy( m_vPlayerInfo, pi ) - pi->GetPlayerStageStats()->m_iSongsPlayed--; - - LoadNextSong(); -} - -void ScreenGameplay::LoadNextSong() -{ - GAMESTATE->ResetMusicStatistics(); - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - pi->GetPlayerStageStats()->m_iSongsPlayed++; - if( pi->m_ptextCourseSongNumber ) - pi->m_ptextCourseSongNumber->SetText( ssprintf(SONG_NUMBER_FORMAT.GetValue(), pi->GetPlayerStageStats()->m_iSongsPassed+1) ); - } - - if( GAMESTATE->m_bMultiplayer ) - { - FOREACH_ENUM( MultiPlayer, mp ) - this->UpdateStageStats( mp ); - } - - int iPlaySongIndex = GAMESTATE->GetCourseSongIndex(); - iPlaySongIndex %= m_apSongsQueue.size(); - GAMESTATE->m_pCurSong.Set( m_apSongsQueue[iPlaySongIndex] ); - STATSMAN->m_CurStageStats.m_vpPlayedSongs.push_back( GAMESTATE->m_pCurSong ); - - // No need to do this here. We do it in SongFinished(). - //GAMESTATE->RemoveAllActiveAttacks(); - - /* If we're in battery mode, force FailImmediate. We assume in Player::Step - * that failed players can't step. */ - if( GAMESTATE->m_SongOptions.GetCurrent().m_LifeType == SongOptions::LIFE_BATTERY ) - { - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - PO_GROUP_ASSIGN( pi->GetPlayerState()->m_PlayerOptions, ModsLevel_Song, m_FailType, PlayerOptions::FAIL_IMMEDIATE ); - } - - m_textSongOptions.SetText( GAMESTATE->m_SongOptions.GetCurrent().GetString() ); - - SetupSong( iPlaySongIndex ); - - Song* pSong = GAMESTATE->m_pCurSong; - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - Steps* pSteps = GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ]; - ++pi->GetPlayerStageStats()->m_iStepsPlayed; - - ASSERT( GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ] ); - if( pi->m_ptextStepsDescription ) - pi->m_ptextStepsDescription->SetText( pSteps->GetDescription() ); - - /* Increment the play count even if the player fails. (It's still popular, - * even if the people playing it aren't good at it.) */ - if( !GAMESTATE->m_bDemonstrationOrJukebox ) - { - if( pi->m_pn != PLAYER_INVALID ) - PROFILEMAN->IncrementStepsPlayCount( pSong, pSteps, pi->m_pn ); - } - - if( pi->m_ptextPlayerOptions ) - pi->m_ptextPlayerOptions->SetText( pi->GetPlayerState()->m_PlayerOptions.GetCurrent().GetString() ); - if( pi->m_pActiveAttackList ) - pi->m_pActiveAttackList->Refresh(); - - // reset oni game over graphic - SET_XY_AND_ON_COMMAND( pi->m_sprOniGameOver ); - - if( GAMESTATE->m_SongOptions.GetCurrent().m_LifeType==SongOptions::LIFE_BATTERY && pi->GetPlayerStageStats()->m_bFailed ) // already failed - pi->ShowOniGameOver(); - - if( GAMESTATE->m_SongOptions.GetCurrent().m_LifeType==SongOptions::LIFE_BAR && pi->m_pLifeMeter ) - pi->m_pLifeMeter->UpdateNonstopLifebar(); - - if( pi->m_pStepsDisplay ) - pi->m_pStepsDisplay->SetFromSteps( pSteps ); - - /* The actual note data for scoring is the base class of Player. This includes - * transforms, like Wide. Otherwise, the scoring will operate on the wrong data. */ - if( pi->m_pPrimaryScoreKeeper ) - pi->m_pPrimaryScoreKeeper->OnNextSong( GAMESTATE->GetCourseSongIndex(), pSteps, &pi->m_pPlayer->GetNoteData() ); - if( pi->m_pSecondaryScoreKeeper ) - pi->m_pSecondaryScoreKeeper->OnNextSong( GAMESTATE->GetCourseSongIndex(), pSteps, &pi->m_pPlayer->GetNoteData() ); - - // Don't mess with the PlayerController of the Dummy player - if( !pi->m_bIsDummy ) - { - if( GAMESTATE->m_bDemonstrationOrJukebox ) - { - pi->GetPlayerState()->m_PlayerController = PC_CPU; - pi->GetPlayerState()->m_iCpuSkill = 5; - } - else if( GAMESTATE->IsCpuPlayer(pi->GetStepsAndTrailIndex()) ) - { - pi->GetPlayerState()->m_PlayerController = PC_CPU; - int iMeter = pSteps->GetMeter(); - int iNewSkill = SCALE( iMeter, MIN_METER, MAX_METER, 0, NUM_SKILL_LEVELS-1 ); - /* Watch out: songs aren't actually bound by MAX_METER. */ - iNewSkill = clamp( iNewSkill, 0, NUM_SKILL_LEVELS-1 ); - pi->GetPlayerState()->m_iCpuSkill = iNewSkill; - } - else - { - if( pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_fPlayerAutoPlay != 0 ) - pi->GetPlayerState()->m_PlayerController = PC_AUTOPLAY; - else - pi->GetPlayerState()->m_PlayerController = GamePreferences::m_AutoPlay; - } - } - } - - bool bAllReverse = true; - bool bAtLeastOneReverse = false; - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_fScrolls[PlayerOptions::SCROLL_REVERSE] == 1 ) - bAtLeastOneReverse = true; - else - bAllReverse = false; - } - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - bool bReverse = pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_fScrolls[PlayerOptions::SCROLL_REVERSE] == 1; - - if( pi->m_pStepsDisplay ) - pi->m_pStepsDisplay->PlayCommand( bReverse? "SetReverse":"SetNoReverse" ); - } - - m_LyricDisplay.PlayCommand( bAllReverse? "SetReverse": bAtLeastOneReverse? "SetOneReverse": "SetNoReverse" ); - - // Load lyrics - // XXX: don't load this here (who and why? -aj) - LyricsLoader LL; - if( GAMESTATE->m_pCurSong->HasLyrics() ) - LL.LoadFromLRCFile(GAMESTATE->m_pCurSong->GetLyricsPath(), *GAMESTATE->m_pCurSong); - - // Set up song-specific graphics. - - // Check to see if any players are in beginner mode. - // Note: steps can be different if turn modifiers are used. - if( PREFSMAN->m_bShowBeginnerHelper ) - { - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - PlayerNumber pn = pi->GetStepsAndTrailIndex(); - if( GAMESTATE->IsHumanPlayer(pn) && GAMESTATE->m_pCurSteps[pn]->GetDifficulty() == Difficulty_Beginner ) - m_BeginnerHelper.AddPlayer( pn, pi->m_pPlayer->GetNoteData() ); - } - } - - if( m_pSongBackground ) - m_pSongBackground->Unload(); - - if( m_pSongForeground ) - m_pSongForeground->Unload(); - - if( !PREFSMAN->m_bShowBeginnerHelper || !m_BeginnerHelper.Init(2) ) - { - m_BeginnerHelper.SetVisible( false ); - - // BeginnerHelper disabled, or failed to load. - if( m_pSongBackground ) - m_pSongBackground->LoadFromSong( GAMESTATE->m_pCurSong ); - - if( !GAMESTATE->m_bDemonstrationOrJukebox ) - { - /* This will fade from a preset brightness to the actual brightness - * (based on prefs and "cover"). The preset brightness may be 0 (to - * fade from black), or it might be 1, if the stage screen has the - * song BG and we're coming from it (like Pump). This used to be done - * in SM_PlayReady, but that means it's impossible to snap to the - * new brightness immediately. */ - if( m_pSongBackground ) - { - m_pSongBackground->SetBrightness( INITIAL_BACKGROUND_BRIGHTNESS ); - m_pSongBackground->FadeToActualBrightness(); - } - } - } - else - { - m_BeginnerHelper.SetVisible( true ); - } - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( !pi->GetPlayerStageStats()->m_bFailed ) - { - // give a little life back between stages - if( pi->m_pLifeMeter ) - pi->m_pLifeMeter->OnLoadSong(); - if( pi->m_pPrimaryScoreDisplay ) - pi->m_pPrimaryScoreDisplay->OnLoadSong(); - if( pi->m_pSecondaryScoreDisplay ) - pi->m_pSecondaryScoreDisplay->OnLoadSong(); - } - } - if( m_pCombinedLifeMeter ) - m_pCombinedLifeMeter->OnLoadSong(); - - if( m_pSongForeground ) - m_pSongForeground->LoadFromSong( GAMESTATE->m_pCurSong ); - - m_fTimeSinceLastDancingComment = 0; - - /* m_soundMusic and m_pSongBackground take a very long time to load, - * so cap fDelta at 0 so m_NextSong will show up on screen. - * -Chris */ - m_bZeroDeltaOnNextUpdate = true; - SCREENMAN->ZeroNextUpdate(); - - // Load cabinet lights data - LoadLights(); - - /* Load the music last, since it may start streaming and we don't want the music - * to compete with other loading. */ - m_AutoKeysounds.FinishLoading(); - m_pSoundMusic = m_AutoKeysounds.GetSound(); - - /* Give SoundEffectControls the new RageSoundReaders. */ - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - RageSoundReader *pPlayerSound = m_AutoKeysounds.GetPlayerSound(pi->m_pn); - if( pPlayerSound == NULL && pi->m_pn == GAMESTATE->GetMasterPlayerNumber() ) - pPlayerSound = m_AutoKeysounds.GetSharedSound(); - pi->m_SoundEffectControl.SetSoundReader( pPlayerSound ); - } - - MESSAGEMAN->Broadcast("DoneLoadingNextSong"); -} - -void ScreenGameplay::LoadLights() -{ - if( !LIGHTSMAN->IsEnabled() ) - return; - - // First, check if the song has explicit lights - m_CabinetLightsNoteData.Init(); - ASSERT( GAMESTATE->m_pCurSong ); - - const Steps *pSteps = SongUtil::GetClosestNotes( GAMESTATE->m_pCurSong, StepsType_lights_cabinet, Difficulty_Medium ); - if( pSteps != NULL ) - { - pSteps->GetNoteData( m_CabinetLightsNoteData ); - return; - } - - // No explicit lights. Create autogen lights. - RString sDifficulty = PREFSMAN->m_sLightsStepsDifficulty; - vector asDifficulties; - split( sDifficulty, ",", asDifficulties ); - - // Always use the steps from the primary steps type so that lights are consistent over single and double styles. - StepsType st = GAMEMAN->GetHowToPlayStyleForGame( GAMESTATE->m_pCurGame )->m_StepsType; - - Difficulty d1 = Difficulty_Invalid; - if( asDifficulties.size() > 0 ) - { - if( asDifficulties[0].CompareNoCase("selected") == 0 ) - { - // Base lights off current difficulty of active player - // Can be either P1 or P2 if they're individual or P1 if both are active - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - PlayerNumber pn = pi->GetStepsAndTrailIndex(); - - if( GAMESTATE->IsPlayerEnabled(pn) ) - { - d1 = GAMESTATE->m_pCurSteps[pn]->GetDifficulty(); - break; - } - } - } - else - d1 = StringToDifficulty( asDifficulties[0] ); - } - - pSteps = SongUtil::GetClosestNotes( GAMESTATE->m_pCurSong, st, d1 ); - - // If we can't find anything at all, stop. - if( pSteps == NULL ) - return; - - NoteData TapNoteData1; - pSteps->GetNoteData( TapNoteData1 ); - - if( asDifficulties.size() > 1 ) - { - Difficulty d2 = Difficulty_Invalid; - - // We've also specified for Player 2 to be based off current difficulty - if( asDifficulties[1].CompareNoCase("selected") == 0 && GAMESTATE->GetNumPlayersEnabled() > 1 ) - { - // Base lights off current difficulty of active player - // Only do this for P2 in a two-player situation, since P1 is taken care of above - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - PlayerNumber pn = pi->GetStepsAndTrailIndex(); - - if( pn == PLAYER_2 ) - d2 = GAMESTATE->m_pCurSteps[pn]->GetDifficulty(); - } - } - else - d2 = StringToDifficulty( asDifficulties[1] ); - - /* fall through */ - } - - NoteDataUtil::LoadTransformedLights( TapNoteData1, m_CabinetLightsNoteData, GAMEMAN->GetStepsTypeInfo(StepsType_lights_cabinet).iNumTracks ); -} - -void ScreenGameplay::StartPlayingSong( float fMinTimeToNotes, float fMinTimeToMusic ) -{ - ASSERT( fMinTimeToNotes >= 0 ); - ASSERT( fMinTimeToMusic >= 0 ); - - m_pSoundMusic->SetProperty( "AccurateSync", true ); - - RageSoundParams p; - p.m_fSpeed = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; - p.StopMode = RageSoundParams::M_CONTINUE; - - { - const float fFirstSecond = GAMESTATE->m_pCurSong->GetFirstSecond(); - float fStartDelay = fMinTimeToNotes - fFirstSecond; - fStartDelay = max( fStartDelay, fMinTimeToMusic ); - p.m_StartSecond = -fStartDelay; - } - - ASSERT( !m_pSoundMusic->IsPlaying() ); - { - float fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut; - GetMusicEndTiming( fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut ); - - if( fSecondsToStartFadingOutMusic < GAMESTATE->m_pCurSong->m_fMusicLengthSeconds ) - { - p.m_fFadeOutSeconds = MUSIC_FADE_OUT_SECONDS; - p.m_LengthSeconds = fSecondsToStartFadingOutMusic + MUSIC_FADE_OUT_SECONDS - p.m_StartSecond; - } - } - m_pSoundMusic->Play( &p ); - if( m_bPaused ) - m_pSoundMusic->Pause( true ); - - /* Make sure GAMESTATE->m_fMusicSeconds is set up. */ - GAMESTATE->m_Position.m_fMusicSeconds = -5000; - UpdateSongPosition(0); - - ASSERT( GAMESTATE->m_Position.m_fMusicSeconds > -4000 ); /* make sure the "fake timer" code doesn't trigger */ -} - - -void ScreenGameplay::PauseGame( bool bPause, GameController gc ) -{ - if( m_bPaused == bPause ) - { - LOG->Trace( "ScreenGameplay::PauseGame(%i) received, but already in that state; ignored", bPause ); - return; - } - - // Don't pause if we're already tweening out. - if( bPause && m_DancingState == STATE_OUTRO ) - return; - - AbortGiveUp( false ); - - m_bPaused = bPause; - m_PauseController = gc; - - m_pSoundMusic->Pause( bPause ); - if( bPause ) - this->PlayCommand( "Pause" ); - else - this->PlayCommand( "Unpause" ); - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - pi->m_pPlayer->SetPaused( m_bPaused ); - } -} - -// play assist ticks -void ScreenGameplay::PlayTicks() -{ - /* TODO: Allow all players to have ticks. Not as simple as it looks. - * If a loop takes place, it could make one player's ticks come later - * than intended. Any help here would be appreciated. -Wolfman2000 */ - Player &player = *m_vPlayerInfo[GAMESTATE->GetMasterPlayerNumber()].m_pPlayer; - const NoteData &nd = player.GetNoteData(); - m_GameplayAssist.PlayTicks( nd, player.GetPlayerState() ); -} - -/* Play announcer "type" if it's been at least fSeconds since the last announcer. */ -void ScreenGameplay::PlayAnnouncer( const RString &type, float fSeconds, float *fDeltaSeconds ) -{ - if( GAMESTATE->m_fOpponentHealthPercent == 0 ) - return; // Shut the announcer up - - /* Don't play in demonstration. */ - if( GAMESTATE->m_bDemonstrationOrJukebox ) - return; - - /* Don't play before the first beat, or after we're finished. */ - if( m_DancingState != STATE_DANCING ) - return; - if(GAMESTATE->m_pCurSong == NULL || // this will be true on ScreenDemonstration sometimes - GAMESTATE->m_Position.m_fSongBeat < GAMESTATE->m_pCurSong->GetFirstBeat()) - return; - - if( *fDeltaSeconds < fSeconds ) - return; - *fDeltaSeconds = 0; - - SOUND->PlayOnceFromAnnouncer( type ); -} - -void ScreenGameplay::UpdateSongPosition( float fDeltaTime ) -{ - if( !m_pSoundMusic->IsPlaying() ) - return; - - RageTimer tm; - const float fSeconds = m_pSoundMusic->GetPositionSeconds( NULL, &tm ); - const float fAdjust = SOUND->GetFrameTimingAdjustment( fDeltaTime ); - GAMESTATE->UpdateSongPosition( fSeconds+fAdjust, GAMESTATE->m_pCurSong->m_SongTiming, tm+fAdjust, true ); -} - -void ScreenGameplay::BeginScreen() -{ - if( GAMESTATE->m_pCurSong == NULL ) - return; - - ScreenWithMenuElements::BeginScreen(); - - SOUND->PlayOnceFromAnnouncer( "gameplay intro" ); // crowd cheer - - // Get the transitions rolling - if( GAMESTATE->m_bDemonstrationOrJukebox ) - { - StartPlayingSong( MIN_SECONDS_TO_STEP, MIN_SECONDS_TO_MUSIC ); - } - else if( NSMAN->useSMserver ) - { - // If we're using networking, we must not have any delay. If we do, - // this can cause inconsistency on different computers and - // different themes. - - StartPlayingSong( 0, 0 ); - m_pSoundMusic->Stop(); - - float startOffset = g_fNetStartOffset; - - NSMAN->StartRequest(1); - - RageSoundParams p; - p.m_fSpeed = 1.0f; // Force 1.0 playback speed - p.StopMode = RageSoundParams::M_CONTINUE; - p.m_StartSecond = startOffset; - m_pSoundMusic->SetProperty( "AccurateSync", true ); - m_pSoundMusic->Play( &p ); - - UpdateSongPosition(0); - } - else - { - StartPlayingSong( MIN_SECONDS_TO_STEP, MIN_SECONDS_TO_MUSIC ); - } -} - -bool ScreenGameplay::AllAreFailing() -{ - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( pi->m_pLifeMeter && !pi->m_pLifeMeter->IsFailing() ) - return false; - } - return true; -} - -void ScreenGameplay::GetMusicEndTiming( float &fSecondsToStartFadingOutMusic, float &fSecondsToStartTransitioningOut ) -{ - float fLastStepSeconds = GAMESTATE->m_pCurSong->GetLastSecond(); - fLastStepSeconds += Player::GetMaxStepDistanceSeconds(); - - float fTransitionLength; - if( !GAMESTATE->IsCourseMode() || IsLastSong() ) - fTransitionLength = OUT_TRANSITION_LENGTH; - else - fTransitionLength = COURSE_TRANSITION_LENGTH; - - fSecondsToStartTransitioningOut = fLastStepSeconds; - - // Align the end of the music fade to the end of the transition. - float fSecondsToFinishFadingOutMusic = fSecondsToStartTransitioningOut + fTransitionLength; - if( fSecondsToFinishFadingOutMusic < GAMESTATE->m_pCurSong->m_fMusicLengthSeconds ) - fSecondsToStartFadingOutMusic = fSecondsToFinishFadingOutMusic - MUSIC_FADE_OUT_SECONDS; - else - fSecondsToStartFadingOutMusic = GAMESTATE->m_pCurSong->m_fMusicLengthSeconds; // don't fade - - /* Make sure we keep going long enough to register a miss for the last note, and - * never start fading before the last note. */ - fSecondsToStartFadingOutMusic = max( fSecondsToStartFadingOutMusic, fLastStepSeconds ); - fSecondsToStartTransitioningOut = max( fSecondsToStartTransitioningOut, fLastStepSeconds ); - - /* Make sure the fade finishes before the transition finishes. */ - fSecondsToStartTransitioningOut = max( fSecondsToStartTransitioningOut, fSecondsToStartFadingOutMusic + MUSIC_FADE_OUT_SECONDS - fTransitionLength ); -} - -void ScreenGameplay::Update( float fDeltaTime ) -{ - if( GAMESTATE->m_pCurSong == NULL ) - { - /* ScreenDemonstration will move us to the next screen. We just need to - * survive for one update without crashing. We need to call Screen::Update - * to make sure we receive the next-screen message. */ - Screen::Update( fDeltaTime ); - return; - } - - UpdateSongPosition( fDeltaTime ); - - if( m_bZeroDeltaOnNextUpdate ) - { - Screen::Update( 0 ); - m_bZeroDeltaOnNextUpdate = false; - } - else - { - Screen::Update( fDeltaTime ); - } - - /* This happens if ScreenDemonstration::HandleScreenMessage sets a new screen when - * PREFSMAN->m_bDelayedScreenLoad. */ - if( GAMESTATE->m_pCurSong == NULL ) - return; - /* This can happen if ScreenDemonstration::HandleScreenMessage sets a new screen when - * !PREFSMAN->m_bDelayedScreenLoad. (The new screen was loaded when we called Screen::Update, - * and the ctor might set a new GAMESTATE->m_pCurSong, so the above check can fail.) */ - if( SCREENMAN->GetTopScreen() != this ) - return; - - /* Update actors when paused, but never move on to another state. */ - if( m_bPaused ) - return; - - //LOG->Trace( "m_fOffsetInBeats = %f, m_fBeatsPerSecond = %f, m_Music.GetPositionSeconds = %f", m_fOffsetInBeats, m_fBeatsPerSecond, m_Music.GetPositionSeconds() ); - - m_AutoKeysounds.Update(fDeltaTime); - - // update GameState HealthState - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - HealthState &hs = pi->GetPlayerState()->m_HealthState; - HealthState OldHealthState = hs; - if( GAMESTATE->GetPlayerFailType(pi->GetPlayerState()) != PlayerOptions::FAIL_OFF && - pi->m_pLifeMeter && pi->m_pLifeMeter->IsFailing() ) - { - hs = HealthState_Dead; - } - else if( pi->m_pLifeMeter && pi->m_pLifeMeter->IsHot() ) - { - hs = HealthState_Hot; - } - else if( GAMESTATE->GetPlayerFailType(pi->GetPlayerState()) != PlayerOptions::FAIL_OFF && - pi->m_pLifeMeter && pi->m_pLifeMeter->IsInDanger() ) - { - hs = HealthState_Danger; - } - else - { - hs = HealthState_Alive; - } - - if( hs != OldHealthState ) - { - Message msg( "HealthStateChanged" ); - msg.SetParam( "PlayerNumber", pi->m_pn ); - msg.SetParam( "HealthState", hs ); - msg.SetParam( "OldHealthState", OldHealthState ); - MESSAGEMAN->Broadcast( msg ); - } - - pi->m_SoundEffectControl.Update( fDeltaTime ); - } - - { - float fSpeed = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; - if( GAMESTATE->m_SongOptions.GetCurrent().m_fHaste != 0.0f ) - fSpeed *= GetHasteRate(); - - RageSoundParams p = m_pSoundMusic->GetParams(); - if( fabsf(p.m_fSpeed - fSpeed) > 0.01f ) - { - p.m_fSpeed = fSpeed; - m_pSoundMusic->SetParams( p ); - } - } - - switch( m_DancingState ) - { - case STATE_DANCING: - { - /* Set STATSMAN->m_CurStageStats.bFailed for failed players. In, FAIL_IMMEDIATE, send - * SM_BeginFailed if all players failed, and kill dead Oni players. */ - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - PlayerNumber pn = pi->GetStepsAndTrailIndex(); - - PlayerOptions::FailType ft = GAMESTATE->GetPlayerFailType( pi->GetPlayerState() ); - SongOptions::LifeType lt = GAMESTATE->m_SongOptions.GetCurrent().m_LifeType; - - if( ft == PlayerOptions::FAIL_OFF || ft == PlayerOptions::FAIL_AT_END ) - continue; - - // check for individual fail - if( pi->m_pLifeMeter == NULL || !pi->m_pLifeMeter->IsFailing() ) - continue; /* isn't failing */ - if( pi->GetPlayerStageStats()->m_bFailed ) - continue; /* failed and is already dead */ - - LOG->Trace("Player %d failed", (int)pn); - pi->GetPlayerStageStats()->m_bFailed = true; // fail - - { - Message msg("PlayerFailed"); - msg.SetParam( "PlayerNumber", pi->m_pn ); - MESSAGEMAN->Broadcast( msg ); - } - - // Check for and do Oni die. - bool bAllowOniDie = false; - switch( lt ) - { - case SongOptions::LIFE_BATTERY: - bAllowOniDie = true; - default: - break; - } - if( bAllowOniDie && ft == PlayerOptions::FAIL_IMMEDIATE ) - { - if( !STATSMAN->m_CurStageStats.AllFailed() ) // if not the last one to fail - { - // kill them! - SOUND->PlayOnceFromDir( THEME->GetPathS(m_sName,"oni die") ); - pi->ShowOniGameOver(); - int tracks = pi->m_NoteData.GetNumTracks(); - pi->m_NoteData.Init(); // remove all notes and scoring - pi->m_NoteData.SetNumTracks(tracks); // reset the number of tracks. - pi->m_pPlayer->FadeToFail(); // tell the NoteField to fade to white - } - } - } - - bool bAllFailed = true; - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - PlayerOptions::FailType ft = GAMESTATE->GetPlayerFailType( pi->GetPlayerState() ); - switch( ft ) - { - case PlayerOptions::FAIL_IMMEDIATE: - if( pi->m_pLifeMeter == NULL || (pi->m_pLifeMeter && !pi->m_pLifeMeter->IsFailing()) ) - bAllFailed = false; - break; - case PlayerOptions::FAIL_IMMEDIATE_CONTINUE: - case PlayerOptions::FAIL_AT_END: - bAllFailed = false; // wait until the end of the song to fail. - break; - case PlayerOptions::FAIL_OFF: - bAllFailed = false; // never fail. - break; - default: - FAIL_M("Invalid fail type! Aborting..."); - } - } - - if( bAllFailed ) - { - m_pSoundMusic->StopPlaying(); - SCREENMAN->PostMessageToTopScreen( SM_NotesEnded, 0 ); - m_LyricDisplay.Stop(); - } - - // Update living players' alive time - // HACK: Don't scale alive time when using tab/tilde. Instead of accumulating time from a timer, - // this time should instead be tied to the music position. - float fUnscaledDeltaTime = m_timerGameplaySeconds.GetDeltaTime(); - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - if( !pi->GetPlayerStageStats()->m_bFailed ) - pi->GetPlayerStageStats()->m_fAliveSeconds += fUnscaledDeltaTime * GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; - - // update fGameplaySeconds - STATSMAN->m_CurStageStats.m_fGameplaySeconds += fUnscaledDeltaTime; - float curBeat = GAMESTATE->m_Position.m_fSongBeat; - Song &s = *GAMESTATE->m_pCurSong; - - if( curBeat >= s.GetFirstBeat() && curBeat < s.GetLastBeat() ) - { - STATSMAN->m_CurStageStats.m_fStepsSeconds += fUnscaledDeltaTime; - - if( GAMESTATE->m_SongOptions.GetCurrent().m_fHaste != 0.0f ) - { - float fHasteRate = GetHasteRate(); - GAMESTATE->m_fAccumulatedHasteSeconds += (fUnscaledDeltaTime * fHasteRate) - fUnscaledDeltaTime; - } - } - - // Check for end of song - { - float fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut; - GetMusicEndTiming( fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut ); - - bool bAllReallyFailed = STATSMAN->m_CurStageStats.AllFailed(); - if( bAllReallyFailed ) - fSecondsToStartTransitioningOut += BEGIN_FAILED_DELAY; - - if( GAMESTATE->m_Position.m_fMusicSeconds >= fSecondsToStartTransitioningOut && !m_NextSong.IsTransitioning() ) - this->PostScreenMessage( SM_NotesEnded, 0 ); - } - - // update 2d dancing characters - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - DancingCharacters *pCharacter = NULL; - if( m_pSongBackground ) - pCharacter = m_pSongBackground->GetDancingCharacters(); - if( pCharacter != NULL ) - { - TapNoteScore tns = pi->m_pPlayer->GetLastTapNoteScore(); - - ANIM_STATES_2D state = AS2D_MISS; - - switch( tns ) - { - case TNS_W4: - case TNS_W3: - state = AS2D_GOOD; - break; - case TNS_W2: - case TNS_W1: - state = AS2D_GREAT; - break; - default: - state = AS2D_MISS; - break; - } - - if( state == AS2D_GREAT && pi->GetPlayerState()->m_HealthState == HealthState_Hot ) - state = AS2D_FEVER; - - pCharacter->Change2DAnimState( pi->m_pn, state ); - } - } - - // Check for enemy death in enemy battle - static float fLastSeenEnemyHealth = 1; - if( fLastSeenEnemyHealth != GAMESTATE->m_fOpponentHealthPercent ) - { - fLastSeenEnemyHealth = GAMESTATE->m_fOpponentHealthPercent; - - if( GAMESTATE->m_fOpponentHealthPercent == 0 ) - { - // HACK: Load incorrect directory on purpose for now. - PlayAnnouncer( "gameplay battle damage level3", 0 ); - - GAMESTATE->RemoveAllActiveAttacks(); - - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - if( !GAMESTATE->IsCpuPlayer(pi->m_pn) ) - continue; - - SOUND->PlayOnceFromDir( THEME->GetPathS(m_sName,"oni die") ); - pi->ShowOniGameOver(); - pi->m_NoteData.Init(); // remove all notes and scoring - pi->m_pPlayer->FadeToFail(); // tell the NoteField to fade to white - } - } - } - - // update give up - bool bGiveUpTimerFired = !m_GiveUpTimer.IsZero() && m_GiveUpTimer.Ago() > 2.5f; - - - bool bAllHumanHaveBigMissCombo = true; - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - if (pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_FailType == PlayerOptions::FAIL_OFF || - pi->GetPlayerState()->m_HealthState < HealthState_Dead ) - { - bAllHumanHaveBigMissCombo = false; - break; - } - } - if (bAllHumanHaveBigMissCombo) // possible to get in here. - { - bAllHumanHaveBigMissCombo = FAIL_ON_MISS_COMBO.GetValue() != -1 && STATSMAN->m_CurStageStats.GetMinimumMissCombo() >= FAIL_ON_MISS_COMBO; - } - if( bGiveUpTimerFired || bAllHumanHaveBigMissCombo ) - { - STATSMAN->m_CurStageStats.m_bGaveUp = true; - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - pi->GetPlayerStageStats()->m_bFailed |= bAllHumanHaveBigMissCombo; - pi->GetPlayerStageStats()->m_bDisqualified |= bGiveUpTimerFired; // Don't disqualify if failing for miss combo. The player should still be eligable for a high score on courses. - } - - AbortGiveUp( false ); - - if( GIVING_UP_GOES_TO_PREV_SCREEN ) - { - BeginBackingOutFromGameplay(); - } - else - { - m_pSoundMusic->StopPlaying(); - this->PostScreenMessage( SM_NotesEnded, 0 ); - } - return; - } - - // Check to see if it's time to play a ScreenGameplay comment - m_fTimeSinceLastDancingComment += fDeltaTime; - - switch( GAMESTATE->m_PlayMode ) - { - case PLAY_MODE_REGULAR: - case PLAY_MODE_BATTLE: - case PLAY_MODE_RAVE: - if( GAMESTATE->OneIsHot() ) - PlayAnnouncer( "gameplay comment hot", SECONDS_BETWEEN_COMMENTS ); - else if( GAMESTATE->AllAreInDangerOrWorse() ) - PlayAnnouncer( "gameplay comment danger", SECONDS_BETWEEN_COMMENTS ); - else - PlayAnnouncer( "gameplay comment good", SECONDS_BETWEEN_COMMENTS ); - break; - case PLAY_MODE_NONSTOP: - case PLAY_MODE_ONI: - case PLAY_MODE_ENDLESS: - PlayAnnouncer( "gameplay comment oni", SECONDS_BETWEEN_COMMENTS ); - break; - default: - ASSERT(0); - } - } - default: break; - } - - PlayTicks(); - - UpdateLights(); - - SendCrossedMessages(); - - if( NSMAN->useSMserver ) - { - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - if( pi->m_pLifeMeter ) - NSMAN->m_playerLife[pi->m_pn] = int(pi->m_pLifeMeter->GetLife()*10000); - - if( m_bShowScoreboard ) - FOREACH_NSScoreBoardColumn(cn) - if( m_bShowScoreboard && NSMAN->ChangedScoreboard(cn) && GAMESTATE->GetFirstDisabledPlayer() != PLAYER_INVALID ) - m_Scoreboard[cn].SetText( NSMAN->m_Scoreboard[cn] ); - } -} - -float ScreenGameplay::GetHasteRate() -{ - if( GAMESTATE->m_Position.m_fMusicSeconds < GAMESTATE->m_fLastHasteUpdateMusicSeconds || // new song - GAMESTATE->m_Position.m_fMusicSeconds > GAMESTATE->m_fLastHasteUpdateMusicSeconds + 4 ) - { - bool bAnyPlayerHitAllNotes = false; - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( !GAMESTATE->IsHumanPlayer(pi->m_pn) ) - continue; - - PlayerState *pPS = pi->GetPlayerState(); - if( pPS->m_iTapsHitSinceLastHasteUpdate > 0 && - pPS->m_iTapsMissedSinceLastHasteUpdate == 0 ) - bAnyPlayerHitAllNotes = true; - - pPS->m_iTapsHitSinceLastHasteUpdate = 0; - pPS->m_iTapsMissedSinceLastHasteUpdate = 0; - } - - if( bAnyPlayerHitAllNotes ) - GAMESTATE->m_fHasteRate += 0.1f; - CLAMP( GAMESTATE->m_fHasteRate, -1.0f, +1.0f ); - - GAMESTATE->m_fLastHasteUpdateMusicSeconds = GAMESTATE->m_Position.m_fMusicSeconds; - } - - /* If the life meter is less than half full, push the haste rate down to let - * the player use his accumulated haste time. */ - float fMaxLife = 0; - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( !GAMESTATE->IsHumanPlayer(pi->m_pn) ) - continue; - fMaxLife = max( fMaxLife, pi->m_pLifeMeter->GetLife() ); - } - if( fMaxLife < 0.5f ) - GAMESTATE->m_fHasteRate = SCALE( fMaxLife, 0.0f, 0.5f, -1.0f, 0.0f ); - - float fSpeed = 1.0f; - if( GAMESTATE->m_fHasteRate < 0 ) - fSpeed = SCALE( GAMESTATE->m_fHasteRate, -1.0f, 0.0f, 0.5f, 1.0f ); - else if( GAMESTATE->m_fHasteRate < 0.3f ) - fSpeed = SCALE( GAMESTATE->m_fHasteRate, 0.0f, 0.3f, 1.0f, 1.2f ); - else - fSpeed = SCALE( GAMESTATE->m_fHasteRate, 0.3f, 1.0f, 1.2f, 1.5f ); - fSpeed *= GAMESTATE->m_SongOptions.GetCurrent().m_fHaste; - - if( GAMESTATE->m_fAccumulatedHasteSeconds <= 1 ) - { - /* Only allow slowing down the song while the players have accumulated - * haste. This prevents dragging on the song by keeping the life meter - * nearly empty. */ - float fClamped = max( 1.0f, fSpeed ); - fSpeed = lerp( GAMESTATE->m_fAccumulatedHasteSeconds, fClamped, fSpeed ); - } - return fSpeed; -} - -void ScreenGameplay::UpdateLights() -{ - if( !LIGHTSMAN->IsEnabled() ) - return; - if( m_CabinetLightsNoteData.GetNumTracks() == 0 ) // light data wasn't loaded - return; - - const Style* pStyle = GAMESTATE->GetCurrentStyle(); - bool bBlinkCabinetLight[NUM_CabinetLight]; - bool bBlinkGameButton[NUM_GameController][NUM_GameButton]; - ZERO( bBlinkCabinetLight ); - ZERO( bBlinkGameButton ); - bool bCrossedABeat = false; - { - const float fSongBeat = GAMESTATE->m_Position.m_fLightSongBeat; - const int iSongRow = BeatToNoteRowNotRounded( fSongBeat ); - - static int iRowLastCrossed = 0; - - float fBeatLast = roundf(NoteRowToBeat(iRowLastCrossed)); - float fBeatNow = roundf(NoteRowToBeat(iSongRow)); - - bCrossedABeat = fBeatLast != fBeatNow; - - FOREACH_CabinetLight( cl ) - { - // for each index we crossed since the last update: - FOREACH_NONEMPTY_ROW_IN_TRACK_RANGE( m_CabinetLightsNoteData, cl, r, iRowLastCrossed+1, iSongRow+1 ) - { - if( m_CabinetLightsNoteData.GetTapNote( cl, r ).type != TapNote::empty ) - bBlinkCabinetLight[cl] = true; - } - - if( m_CabinetLightsNoteData.IsHoldNoteAtRow( cl, iSongRow ) ) - bBlinkCabinetLight[cl] = true; - } - - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - const NoteData &nd = pi->m_pPlayer->GetNoteData(); - for( int t=0; tStyleInputToGameInput( t, pi->m_pn ); - bBlinkGameButton[gi.controller][gi.button] = true; - } - } - } - - iRowLastCrossed = iSongRow; - } - - // Before the first beat of the song, all cabinet lights solid on (except for menu buttons). - Song &s = *GAMESTATE->m_pCurSong; - bool bOverrideCabinetBlink = (GAMESTATE->m_Position.m_fSongBeat < s.GetFirstBeat()); - FOREACH_CabinetLight( cl ) - bBlinkCabinetLight[cl] |= bOverrideCabinetBlink; - - // Send blink data. - FOREACH_CabinetLight( cl ) - { - if( bBlinkCabinetLight[cl] ) - LIGHTSMAN->BlinkCabinetLight( cl ); - } - - FOREACH_ENUM( GameController, gc ) - { - FOREACH_ENUM( GameButton, gb ) - { - if( bBlinkGameButton[gc][gb] ) - LIGHTSMAN->BlinkGameButton( GameInput(gc,gb) ); - } - } -} - -void ScreenGameplay::SendCrossedMessages() -{ - { - static int iRowLastCrossed = 0; - - float fPositionSeconds = GAMESTATE->m_Position.m_fMusicSeconds; - float fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds ); - - int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); - iRowNow = max( 0, iRowNow ); - - for( int r=iRowLastCrossed+1; r<=iRowNow; r++ ) - { - if( GetNoteType( r ) == NOTE_TYPE_4TH ) - MESSAGEMAN->Broadcast( Message_BeatCrossed ); - } - - iRowLastCrossed = iRowNow; - } - - { - const int NUM_MESSAGES_TO_SEND = 4; - const float MESSAGE_SPACING_SECONDS = 0.4f; - - PlayerNumber pn = PLAYER_INVALID; - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - if( GAMESTATE->m_pCurSteps[ pi->m_pn ]->GetDifficulty() == Difficulty_Beginner ) - { - pn = pi->m_pn; - break; - } - } - if( pn == PLAYER_INVALID ) - return; - - const NoteData &nd = m_vPlayerInfo[pn].m_pPlayer->GetNoteData(); - - static int iRowLastCrossedAll[NUM_MESSAGES_TO_SEND] = { 0, 0, 0, 0 }; - for( int i=0; im_Position.m_fMusicSeconds + fNoteWillCrossInSeconds; - float fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds ); - - int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); - iRowNow = max( 0, iRowNow ); - int &iRowLastCrossed = iRowLastCrossedAll[i]; - - FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( nd, r, iRowLastCrossed+1, iRowNow+1 ) - { - int iNumTracksWithTapOrHoldHead = 0; - for( int t=0; tGetCurrentStyle(); - RString sButton = pStyle->ColToButtonName( t ); - Message msg( i == 0 ? "NoteCrossed" : "NoteWillCross" ); - msg.SetParam( "ButtonName", sButton ); - msg.SetParam( "NumMessagesFromCrossed", i ); - MESSAGEMAN->Broadcast( msg ); - } - - if( iNumTracksWithTapOrHoldHead > 0 ) - MESSAGEMAN->Broadcast( (MessageID)(Message_NoteCrossed + i) ); - if( i == 0 && iNumTracksWithTapOrHoldHead >= 2 ) - { - RString sMessageName = "NoteCrossedJump"; - MESSAGEMAN->Broadcast( sMessageName ); - } - } - - iRowLastCrossed = iRowNow; - } - } -} - -void ScreenGameplay::BeginBackingOutFromGameplay() -{ - m_DancingState = STATE_OUTRO; - AbortGiveUp( false ); - - m_pSoundMusic->StopPlaying(); - m_GameplayAssist.StopPlaying(); // Stop any queued assist ticks. - - this->ClearMessageQueue(); - - // If this is the final stage, don't allow extra stage - if( GAMESTATE->GetSmallestNumStagesLeftForAnyHumanPlayer() == 0 ) - GAMESTATE->m_bBackedOutOfFinalStage = true; - // Disallow backing out of extra stage - if( GAMESTATE->IsAnExtraStage() ) - SCREENMAN->PostMessageToTopScreen( SM_BeginFailed, 0 ); - else - m_Cancel.StartTransitioning( SM_DoPrevScreen ); -} - -void ScreenGameplay::AbortGiveUp( bool bShowText ) -{ - if( m_GiveUpTimer.IsZero() ) - return; - - m_textDebug.StopTweening(); - if( bShowText ) - m_textDebug.SetText( GIVE_UP_ABORTED_TEXT ); - // otherwise tween out the text that's there - - m_textDebug.BeginTweening( 1/2.f ); - m_textDebug.SetDiffuse( RageColor(1,1,1,0) ); - m_GiveUpTimer.SetZero(); -} - - -void ScreenGameplay::Input( const InputEventPlus &input ) -{ - //LOG->Trace( "ScreenGameplay::Input()" ); - - Message msg(""); - if( m_Codes.InputMessage(input, msg) ) - this->HandleMessage( msg ); - - if( m_bPaused ) - { - /* If we're paused, only accept GAME_BUTTON_START to unpause. */ - if( GAMESTATE->IsHumanPlayer(input.pn) && input.MenuI == GAME_BUTTON_START && input.type == IET_FIRST_PRESS ) - { - if( m_PauseController == GameController_Invalid || m_PauseController == input.GameI.controller ) - { - // IMO, it's better to have this configurable. -DaisuMaster - if( UNPAUSE_WITH_START ) this->PauseGame( false ); - } - } - return; - } - - if( m_DancingState != STATE_OUTRO && - GAMESTATE->IsHumanPlayer(input.pn) && - !m_Cancel.IsTransitioning() ) - { - /* Allow bailing out by holding any START button. - * This gives a way to "give up" when a back button isn't available. - * If this is also a style button, don't do this; pump center is start. - */ - bool bHoldingGiveUp = false; - if( GAMESTATE->GetCurrentStyle()->GameInputToColumn(input.GameI) == Column_Invalid ) - { - bHoldingGiveUp |= ( START_GIVES_UP && input.MenuI == GAME_BUTTON_START ); - bHoldingGiveUp |= ( BACK_GIVES_UP && input.MenuI == GAME_BUTTON_BACK ); - } - - if( bHoldingGiveUp ) - { - // No PREFSMAN->m_bDelayedEscape; always delayed. - if( input.type==IET_RELEASE ) - { - AbortGiveUp( true ); - } - else if( input.type==IET_FIRST_PRESS && m_GiveUpTimer.IsZero() ) - { - m_textDebug.SetText( GIVE_UP_START_TEXT ); - m_textDebug.PlayCommand( "StartOn" ); - m_GiveUpTimer.Touch(); // start the timer - } - - return; - } - - /* Only handle GAME_BUTTON_BACK as a regular BACK button if BACK_GIVES_UP is - * disabled. */ - bool bHoldingBack = false; - if( GAMESTATE->GetCurrentStyle()->GameInputToColumn(input.GameI) == Column_Invalid ) - { - bHoldingBack |= input.MenuI == GAME_BUTTON_BACK && !BACK_GIVES_UP; - } - - if( bHoldingBack ) - { - if( ((!PREFSMAN->m_bDelayedBack && input.type==IET_FIRST_PRESS) || - (input.DeviceI.device==DEVICE_KEYBOARD && input.type==IET_REPEAT) || - (input.DeviceI.device!=DEVICE_KEYBOARD && INPUTFILTER->GetSecsHeld(input.DeviceI) >= 1.0f)) ) - { - LOG->Trace("Player %i went back", input.pn+1); - BeginBackingOutFromGameplay(); - } - else if( PREFSMAN->m_bDelayedBack && input.type==IET_FIRST_PRESS ) - { - m_textDebug.SetText( GIVE_UP_BACK_TEXT ); - m_textDebug.PlayCommand( "BackOn" ); - } - else if( PREFSMAN->m_bDelayedBack && input.type==IET_RELEASE ) - { - m_textDebug.PlayCommand( "TweenOff" ); - } - - return; - } - } - - bool bRelease = input.type == IET_RELEASE; - if( !input.GameI.IsValid() ) - return; - - int iCol = GAMESTATE->GetCurrentStyle()->GameInputToColumn( input.GameI ); - - // Don't pass on any inputs to Player that aren't a press or a release. - switch( input.type ) - { - case IET_FIRST_PRESS: - case IET_RELEASE: - break; - default: - return; - } - - if( GAMESTATE->m_bMultiplayer ) - { - if( input.mp != MultiPlayer_Invalid && GAMESTATE->IsMultiPlayerEnabled(input.mp) && iCol != -1 ) - { - FOREACH( PlayerInfo, m_vPlayerInfo, pi ) - { - if( input.mp == pi->m_mp ) - pi->m_pPlayer->Step( iCol, -1, input.DeviceI.ts, false, bRelease ); - } - } - } - else - { - // handle a step or battle item activate - if( GAMESTATE->IsHumanPlayer( input.pn ) ) - { - AbortGiveUp( true ); - - if( GamePreferences::m_AutoPlay == PC_HUMAN && GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerOptions.GetCurrent().m_fPlayerAutoPlay == 0 ) - { - PlayerInfo& pi = GetPlayerInfoForInput( input ); - - ASSERT( input.GameI.IsValid() ); - - GameButtonType gbt = GAMESTATE->m_pCurGame->GetPerButtonInfo(input.GameI.button)->m_gbt; - switch( gbt ) - { - case GameButtonType_INVALID: - break; - case GameButtonType_Step: - if( iCol != -1 ) - pi.m_pPlayer->Step( iCol, -1, input.DeviceI.ts, false, bRelease ); - break; - case GameButtonType_Fret: - if( iCol != -1 ) - pi.m_pPlayer->Fret( iCol, -1, input.DeviceI.ts, false, bRelease ); - break; - case GameButtonType_Strum: - pi.m_pPlayer->Strum( iCol, -1, input.DeviceI.ts, false, bRelease ); - break; - } - } - } - } -} - - -/* Saving StageStats that are affected by the note pattern is a little tricky: - * - * Stats are cumulative for course play. - * - * For regular songs, it doesn't matter how we do it; the pattern doesn't change - * during play. - * - * The pattern changes during play in battle and course mode. We want to include - * these changes, so run stats for a song after the song finishes. - * - * If we fail, be sure to include the current song in stats, - * with the current modifier set. So: - * 1. At the end of a song in any mode, pass or fail, add stats for that song - * (from m_pPlayer). - * 2. At the end of gameplay in course mode, add stats for any songs that weren't - * played, applying the modifiers the song would have been played with. - * This doesn't include songs that were played but failed; that was done in #1. - */ -void ScreenGameplay::SaveStats() -{ - float fMusicLen = GAMESTATE->m_pCurSong->m_fMusicLengthSeconds; - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - /* Note that adding stats is only meaningful for the counters (eg. RadarCategory_Jumps), - * not for the percentages (RadarCategory_Air). */ - RadarValues rv; - PlayerStageStats &pss = *pi->GetPlayerStageStats(); - const NoteData &nd = pi->m_pPlayer->GetNoteData(); - PlayerNumber pn = pi->m_pn; - - GAMESTATE->SetProcessedTimingData(&GAMESTATE->m_pCurSteps[pn]->m_Timing); - NoteDataUtil::CalculateRadarValues( nd, fMusicLen, rv ); - pss.m_radarPossible += rv; - NoteDataWithScoring::GetActualRadarValues( nd, pss, fMusicLen, rv ); - pss.m_radarActual += rv; - GAMESTATE->SetProcessedTimingData(NULL); - } -} - -void ScreenGameplay::SongFinished() -{ - AdjustSync::HandleSongEnd(); - SaveStats(); // Let subclasses save the stats. - /* Extremely important: if we don't remove attacks before moving on to the next - * screen, they'll still be turned on eventually. */ - GAMESTATE->RemoveAllActiveAttacks(); - FOREACH_VisiblePlayerInfo( m_vPlayerInfo, pi ) - pi->m_pActiveAttackList->Refresh(); -} - -void ScreenGameplay::StageFinished( bool bBackedOut ) -{ - if( GAMESTATE->IsCourseMode() && GAMESTATE->m_PlayMode != PLAY_MODE_ENDLESS ) - { - LOG->Trace("Stage finished at index %i/%i", GAMESTATE->GetCourseSongIndex(), (int) m_apSongsQueue.size() ); - // +1 to skip the current song; that song has already passed. - for( unsigned i = GAMESTATE->GetCourseSongIndex()+1; i < m_apSongsQueue.size(); ++i ) - { - LOG->Trace("Running stats for %i", i ); - SetupSong( i ); - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - pi->m_pPlayer->ApplyWaitingTransforms(); - SongFinished(); - } - } - - if( bBackedOut ) - { - GAMESTATE->CancelStage(); - return; - } - - // If all players failed, kill. - if( STATSMAN->m_CurStageStats.AllFailed() ) - { - FOREACH_HumanPlayer( p ) - GAMESTATE->m_iPlayerStageTokens[p] = 0; - } - - FOREACH_HumanPlayer( pn ) - STATSMAN->m_CurStageStats.m_player[pn].CalcAwards( pn, STATSMAN->m_CurStageStats.m_bGaveUp, STATSMAN->m_CurStageStats.m_bUsedAutoplay ); - STATSMAN->m_CurStageStats.FinalizeScores( false ); - - GAMESTATE->CommitStageStats(); - - // save current stage stats - STATSMAN->m_vPlayedStageStats.push_back( STATSMAN->m_CurStageStats ); - - STATSMAN->CalcAccumPlayedStageStats(); - GAMESTATE->FinishStage(); -} - -void ScreenGameplay::HandleScreenMessage( const ScreenMessage SM ) -{ - CHECKPOINT_M( ssprintf("HandleScreenMessage(%s)", ScreenMessageHelpers::ScreenMessageToString(SM).c_str()) ); - if( SM == SM_DoneFadingIn ) - { - SOUND->PlayOnceFromAnnouncer( "gameplay ready" ); - m_Ready.StartTransitioning( SM_PlayGo ); - } - else if( SM == SM_PlayGo ) - { - if( GAMESTATE->IsAnExtraStage() ) - SOUND->PlayOnceFromAnnouncer( "gameplay here we go extra" ); - else if( GAMESTATE->GetSmallestNumStagesLeftForAnyHumanPlayer() == 0 ) - SOUND->PlayOnceFromAnnouncer( "gameplay here we go final" ); - else - SOUND->PlayOnceFromAnnouncer( "gameplay here we go normal" ); - - m_Go.StartTransitioning( SM_None ); - GAMESTATE->m_bGameplayLeadIn.Set( false ); - m_DancingState = STATE_DANCING; // STATE CHANGE! Now the user is allowed to press Back - } - else if( SM == SM_NotesEnded ) // received while STATE_DANCING - { - AbortGiveUp( false ); // don't allow giveup while the next song is loading - - /* Do this in LoadNextSong, so we don't tween off old attacks until - * m_NextSong finishes. */ - // GAMESTATE->RemoveAllActiveAttacks(); - - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - // Mark failure. - if( GAMESTATE->GetPlayerFailType(pi->GetPlayerState()) != PlayerOptions::FAIL_OFF && - (pi->m_pLifeMeter && pi->m_pLifeMeter->IsFailing()) ) - pi->GetPlayerStageStats()->m_bFailed = true; - - if( !pi->GetPlayerStageStats()->m_bFailed ) - pi->GetPlayerStageStats()->m_iSongsPassed++; - - // set a life record at the point of failure - if( pi->GetPlayerStageStats()->m_bFailed ) - pi->GetPlayerStageStats()->SetLifeRecordAt( 0, STATSMAN->m_CurStageStats.m_fGameplaySeconds ); - } - - /* If all players have *really* failed (bFailed, not the life meter or - * bFailedEarlier): */ - const bool bAllReallyFailed = STATSMAN->m_CurStageStats.AllFailed(); - const bool bStopCourseEarly = STOP_COURSE_EARLY; - const bool bIsLastSong = IsLastSong(); - - LOG->Trace( "bAllReallyFailed = %d, bStopCourseEarly = %d, bIsLastSong = %d", bAllReallyFailed, bStopCourseEarly, bIsLastSong ); - - if( bStopCourseEarly || bAllReallyFailed || bIsLastSong ) - { - // Time to leave from ScreenGameplay - HandleScreenMessage( SM_LeaveGameplay ); - } - else - { - // Load the next song in the course. - HandleScreenMessage( SM_StartLoadingNextSong ); - return; - } - } - else if( SM == SM_LeaveGameplay ) - { - // update dancing characters for win / lose - DancingCharacters *pDancers = NULL; - if( m_pSongBackground ) - pDancers = m_pSongBackground->GetDancingCharacters(); - if( pDancers ) - { - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - // XXX: In battle modes, switch( GAMESTATE->GetStageResult(p) ). - if( pi->GetPlayerStageStats()->m_bFailed ) - pDancers->Change2DAnimState( pi->m_pn, AS2D_FAIL ); // fail anim - else if( pi->m_pLifeMeter && pi->GetPlayerState()->m_HealthState == HealthState_Hot ) - pDancers->Change2DAnimState( pi->m_pn, AS2D_WINFEVER ); // full life pass anim - else - pDancers->Change2DAnimState( pi->m_pn, AS2D_WIN ); // pass anim - } - } - - // End round. - if( m_DancingState == STATE_OUTRO ) // ScreenGameplay already ended - return; // ignore - m_DancingState = STATE_OUTRO; - AbortGiveUp( false ); - - GAMESTATE->RemoveAllActiveAttacks(); - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( pi->m_pActiveAttackList ) - pi->m_pActiveAttackList->Refresh(); - } - - LIGHTSMAN->SetLightsMode( LIGHTSMODE_ALL_CLEARED ); - - bool bAllReallyFailed = STATSMAN->m_CurStageStats.AllFailed(); - - if( bAllReallyFailed ) - { - this->PostScreenMessage( SM_BeginFailed, 0 ); - return; - } - - // todo: add GameplayCleared, StartTransitioningCleared commands -aj - TweenOffScreen(); - - m_Out.StartTransitioning( SM_DoNextScreen ); - - // do they deserve an extra stage? - if( GAMESTATE->HasEarnedExtraStage() ) - SOUND->PlayOnceFromAnnouncer( "gameplay extra" ); - else - SOUND->PlayOnceFromAnnouncer( "gameplay cleared" ); - } - else if( SM == SM_StartLoadingNextSong ) - { - // Next song. - // give a little life back between stages - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( pi->m_pLifeMeter ) - pi->m_pLifeMeter->OnSongEnded(); - } - - GAMESTATE->m_bLoadingNextSong = true; - MESSAGEMAN->Broadcast( "BeforeLoadingNextCourseSong" ); - m_NextSong.Reset(); - m_NextSong.PlayCommand( "Start" ); - m_NextSong.StartTransitioning( SM_LoadNextSong ); - MESSAGEMAN->Broadcast( "ChangeCourseSongIn" ); - } - else if( SM == SM_LoadNextSong ) - { - m_pSoundMusic->Stop(); - SongFinished(); - - MESSAGEMAN->Broadcast( "ChangeCourseSongOut" ); - - GAMESTATE->m_bLoadingNextSong = false; - LoadNextSong(); - - m_NextSong.Reset(); - m_NextSong.PlayCommand( "Finish" ); - m_NextSong.StartTransitioning( SM_None ); - - StartPlayingSong( MIN_SECONDS_TO_STEP_NEXT_SONG, 0 ); - } - else if( SM == SM_PlayToasty ) - { - // todo: make multiple toasties work -aj - if( g_bEasterEggs ) - if( !m_Toasty.IsTransitioning() && !m_Toasty.IsFinished() ) // don't play if we've already played it once - m_Toasty.StartTransitioning(); - } - else if( ScreenMessageHelpers::ScreenMessageToString(SM).find("0Combo") != string::npos ) - { - int iCombo; - RString sCropped = ScreenMessageHelpers::ScreenMessageToString(SM).substr(3); - sscanf(sCropped.c_str(),"%d%*s",&iCombo); - PlayAnnouncer( ssprintf("gameplay %d combo",iCombo), 2 ); - } - else if( SM == SM_ComboStopped ) - { - PlayAnnouncer( "gameplay combo stopped", 2 ); - } - else if( SM == SM_ComboContinuing ) - { - PlayAnnouncer( "gameplay combo overflow", 2 ); - } - else if( SM >= SM_BattleTrickLevel1 && SM <= SM_BattleTrickLevel3 ) - { - int iTrickLevel = SM-SM_BattleTrickLevel1+1; - PlayAnnouncer( ssprintf("gameplay battle trick level%d",iTrickLevel), 3 ); - if( SM == SM_BattleTrickLevel1 ) m_soundBattleTrickLevel1.Play(); - else if( SM == SM_BattleTrickLevel2 ) m_soundBattleTrickLevel2.Play(); - else if( SM == SM_BattleTrickLevel3 ) m_soundBattleTrickLevel3.Play(); - } - else if( SM >= SM_BattleDamageLevel1 && SM <= SM_BattleDamageLevel3 ) - { - int iDamageLevel = SM-SM_BattleDamageLevel1+1; - PlayAnnouncer( ssprintf("gameplay battle damage level%d",iDamageLevel), 3 ); - } - else if( SM == SM_DoPrevScreen ) - { - SongFinished(); - this->StageFinished( true ); - - m_sNextScreen = GetPrevScreen(); - - if( AdjustSync::IsSyncDataChanged() ) - ScreenSaveSync::PromptSaveSync( SM_GoToPrevScreen ); - else - HandleScreenMessage( SM_GoToPrevScreen ); - } - else if( SM == SM_DoNextScreen ) - { - SongFinished(); - this->StageFinished( false ); - // only save replays if the player chose to - if( GAMESTATE->m_SongOptions.GetCurrent().m_bSaveReplay ) - SaveReplay(); - - if( AdjustSync::IsSyncDataChanged() ) - ScreenSaveSync::PromptSaveSync( SM_GoToNextScreen ); - else - HandleScreenMessage( SM_GoToNextScreen ); - } - else if( SM == SM_GainFocus ) - { - // We do this ourself. - SOUND->HandleSongTimer( false ); - } - else if( SM == SM_LoseFocus ) - { - // We might have turned the song timer off. Be sure to turn it back on. - SOUND->HandleSongTimer( true ); - } - else if( SM == SM_BeginFailed ) - { - m_DancingState = STATE_OUTRO; - AbortGiveUp( false ); - m_GameplayAssist.StopPlaying(); // Stop any queued assist ticks. - TweenOffScreen(); - m_Failed.StartTransitioning( SM_DoNextScreen ); - - // show the survive time if extra stage - if( GAMESTATE->IsAnExtraStage() ) - { - float fMaxAliveSeconds = 0; - FOREACH_EnabledPlayer(p) - fMaxAliveSeconds = max( fMaxAliveSeconds, STATSMAN->m_CurStageStats.m_player[p].m_fAliveSeconds ); - m_textSurviveTime.SetText( "TIME: " + SecondsToMMSSMsMs(fMaxAliveSeconds) ); - ON_COMMAND( m_textSurviveTime ); - } - - if( GAMESTATE->IsCourseMode() ) - { - if( GAMESTATE->GetCourseSongIndex() >= int(m_apSongsQueue.size() / 2) ) - SOUND->PlayOnceFromAnnouncer( "gameplay oni failed halfway" ); - else - SOUND->PlayOnceFromAnnouncer( "gameplay oni failed" ); - } - else - { - SOUND->PlayOnceFromAnnouncer( "gameplay failed" ); - } - } - else if( SM == SM_Pause ) - { - // Ignore SM_Pause when in demonstration. - if( GAMESTATE->m_bDemonstrationOrJukebox ) - return; - - if( !m_bPaused ) - PauseGame( true ); - } - - ScreenWithMenuElements::HandleScreenMessage( SM ); -} - -void ScreenGameplay::HandleMessage( const Message &msg ) -{ - if( msg == "Judgment" ) - { - PlayerNumber pn; - msg.GetParam( "Player", pn ); - - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - if( pi->m_pn != pn ) - continue; - if( !pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_bMuteOnError ) - continue; - - RageSoundReader *pSoundReader = m_AutoKeysounds.GetPlayerSound( pn ); - if( pSoundReader == NULL ) - pSoundReader = m_AutoKeysounds.GetSharedSound(); - - HoldNoteScore hns; - msg.GetParam( "HoldNoteScore", hns ); - TapNoteScore tns; - msg.GetParam( "TapNoteScore", tns ); - - bool bOn = false; - if( hns != HoldNoteScore_Invalid ) - bOn = hns != HNS_LetGo; - else - bOn = tns != TNS_Miss; - - if( pSoundReader ) - pSoundReader->SetProperty( "Volume", bOn? 1.0f:0.0f ); - } - } - - ScreenWithMenuElements::HandleMessage( msg ); -} - -void ScreenGameplay::Cancel( ScreenMessage smSendWhenDone ) -{ - m_pSoundMusic->Stop(); - - ScreenWithMenuElements::Cancel( smSendWhenDone ); -} - -Song *ScreenGameplay::GetNextCourseSong() const -{ - ASSERT( GAMESTATE->IsCourseMode() ); - - int iPlaySongIndex = GAMESTATE->GetCourseSongIndex()+1; - iPlaySongIndex %= m_apSongsQueue.size(); - - return m_apSongsQueue[iPlaySongIndex]; -} - -PlayerInfo *ScreenGameplay::GetPlayerInfo( PlayerNumber pn ) -{ - FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) - { - if( pi->m_pn == pn ) - return &*pi; - } - return NULL; -} - -PlayerInfo *ScreenGameplay::GetDummyPlayerInfo( int iDummyIndex ) -{ - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - if( pi->m_bIsDummy && pi->m_iDummyIndex == iDummyIndex ) - return &*pi; - } - return NULL; -} - -void ScreenGameplay::SaveReplay() -{ - /* Replay data TODO: - * Add more player information (?) - * Add AutoGen flag if steps were autogen? - * Add proper steps hash? - * Add modifiers used - * Add date played, machine played on, etc. - * Hash of some stuff to validate data (see Profile) - */ - FOREACH_HumanPlayer( pn ) - { - FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) - { - Profile *pTempProfile = PROFILEMAN->GetProfile(pn); - - XNode *p = new XNode("ReplayData"); - // append version number (in case the format changes) - p->AppendAttr("Version",0); - - // song information node - SongID songID; - songID.FromSong(GAMESTATE->m_pCurSong); - XNode *pSongInfoNode = songID.CreateNode(); - pSongInfoNode->AppendChild("Title", GAMESTATE->m_pCurSong->GetDisplayFullTitle()); - pSongInfoNode->AppendChild("Artist", GAMESTATE->m_pCurSong->GetDisplayArtist()); - p->AppendChild(pSongInfoNode); - - // steps information - StepsID stepsID; - stepsID.FromSteps( GAMESTATE->m_pCurSteps[pn] ); - XNode *pStepsInfoNode = stepsID.CreateNode(); - // hashing = argh - //pStepsInfoNode->AppendChild("StepsHash", stepsID.ToSteps(GAMESTATE->m_pCurSong,false)->GetHash()); - p->AppendChild(pStepsInfoNode); - - // player information node (rival data sup) - XNode *pPlayerInfoNode = new XNode("Player"); - pPlayerInfoNode->AppendChild("DisplayName", pTempProfile->m_sDisplayName); - pPlayerInfoNode->AppendChild("Guid", pTempProfile->m_sGuid); - p->AppendChild(pPlayerInfoNode); - - // the timings. - p->AppendChild( pi->m_pPlayer->GetNoteData().CreateNode() ); - - // Find a file name for the replay - vector files; - GetDirListing( "Save/Replays/replay*", files, false, false ); - sort( files.begin(), files.end() ); - - // Files should be of the form "replay#####.xml". - int iIndex = 0; - - for( int i = files.size()-1; i >= 0; --i ) - { - static Regex re( "^replay([0-9]{5})\\....$" ); - vector matches; - if( !re.Compare( files[i], matches ) ) - continue; - - ASSERT( matches.size() == 1 ); - iIndex = StringToInt( matches[0] )+1; - break; - } - - RString sFileName = ssprintf( "replay%05d.xml", iIndex ); - - XmlFileUtil::SaveToFile( p, "Save/Replays/"+sFileName ); - SAFE_DELETE( p ); - return; - } - } -} - -/* -bool ScreenGameplay::LoadReplay() -{ - // Load replay which was selected via options -} -*/ - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the ScreenGameplay. */ -class LunaScreenGameplay: public Luna -{ -public: - static int GetNextCourseSong( T* p, lua_State *L ) { p->GetNextCourseSong()->PushSelf(L); return 1; } - static int Center1Player( T* p, lua_State *L ) { lua_pushboolean( L, p->Center1Player() ); return 1; } - static int GetLifeMeter( T* p, lua_State *L ) - { - PlayerNumber pn = Enum::Check( L, 1 ); - - PlayerInfo *pi = p->GetPlayerInfo(pn); - if( pi == NULL ) - return 0; - LifeMeter *pLM = pi->m_pLifeMeter; - if( pLM == NULL ) - return 0; - - pLM->PushSelf( L ); - return 1; - } - static int GetPlayerInfo( T* p, lua_State *L ) - { - PlayerNumber pn = Enum::Check( L, 1 ); - - PlayerInfo *pi = p->GetPlayerInfo(pn); - if( pi == NULL ) - return 0; - - pi->PushSelf( L ); - return 1; - } - static int GetDummyPlayerInfo( T* p, lua_State *L ) - { - int iDummyIndex = IArg(1); - PlayerInfo *pi = p->GetDummyPlayerInfo(iDummyIndex); - if( pi == NULL ) - return 0; - pi->PushSelf( L ); - return 1; - } - static int PauseGame( T* p, lua_State *L ) { p->Pause( BArg(1)); return 0; } - static int IsPaused( T* p, lua_State *L ) { lua_pushboolean( L, p->IsPaused() ); return 1; } - - LunaScreenGameplay() - { - ADD_METHOD( GetNextCourseSong ); - ADD_METHOD( Center1Player ); - ADD_METHOD( GetLifeMeter ); - ADD_METHOD( GetPlayerInfo ); - ADD_METHOD( GetDummyPlayerInfo ); - // sm-ssc additions: - ADD_METHOD( PauseGame ); - ADD_METHOD( IsPaused ); - } -}; - -LUA_REGISTER_DERIVED_CLASS( ScreenGameplay, ScreenWithMenuElements ) - - -/** @brief Allow Lua to have access to the PlayerInfo. */ -class LunaPlayerInfo: public Luna -{ -public: - static int GetLifeMeter( T* p, lua_State *L ) { p->m_pLifeMeter->PushSelf(L); return 1; } - static int GetStepsQueueWrapped( T* p, lua_State *L ) - { - int iIndex = IArg(1); - iIndex %= p->m_vpStepsQueue.size(); - Steps *pSteps = p->m_vpStepsQueue[iIndex]; - pSteps->PushSelf(L); - return 1; - } - - LunaPlayerInfo() - { - ADD_METHOD( GetLifeMeter ); - ADD_METHOD( GetStepsQueueWrapped ); - } -}; - -LUA_REGISTER_CLASS( PlayerInfo ) -// lua end - -/* - * (c) 2001-2004 Chris Danford, Glenn Maynard - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#include "global.h" +#include "ScreenGameplay.h" +#include "SongManager.h" +#include "ScreenManager.h" +#include "GameConstantsAndTypes.h" +#include "PrefsManager.h" +#include "GamePreferences.h" +#include "GameManager.h" +#include "RageFileManager.h" +#include "Steps.h" +#include "RageLog.h" +#include "LifeMeter.h" +#include "LifeMeterBar.h" +#include "GameState.h" +#include "ScoreDisplayNormal.h" +#include "ScoreDisplayPercentage.h" +#include "ScoreDisplayLifeTime.h" +#include "ScoreDisplayOni.h" +#include "ScoreDisplayRave.h" +#include "ThemeManager.h" +#include "RageTimer.h" +#include "ScoreKeeperNormal.h" +#include "ScoreKeeperRave.h" +#include "LyricsLoader.h" +#include "ActorUtil.h" +#include "RageSoundManager.h" +#include "RageSoundReader.h" +#include "RageTextureManager.h" +#include "GameSoundManager.h" +#include "CombinedLifeMeterTug.h" +#include "Inventory.h" +#include "Course.h" +#include "NoteDataUtil.h" +#include "UnlockManager.h" +#include "LightsManager.h" +#include "ProfileManager.h" +#include "StatsManager.h" +#include "PlayerAI.h" // for NUM_SKILL_LEVELS +#include "NetworkSyncManager.h" +#include "Foreach.h" +#include "DancingCharacters.h" +#include "ScreenDimensions.h" +#include "ThemeMetric.h" +#include "PlayerState.h" +#include "Style.h" +#include "LuaManager.h" +#include "MemoryCardManager.h" +#include "CommonMetrics.h" +#include "InputMapper.h" +#include "Game.h" +#include "ActiveAttackList.h" +#include "Player.h" +#include "StepsDisplay.h" +#include "XmlFile.h" +#include "Background.h" +#include "Foreground.h" +#include "ScreenSaveSync.h" +#include "AdjustSync.h" +#include "SongUtil.h" +#include "Song.h" +#include "XmlFileUtil.h" +#include "Profile.h" // for replay data stuff + +// Defines +#define SHOW_LIFE_METER_FOR_DISABLED_PLAYERS THEME->GetMetricB(m_sName,"ShowLifeMeterForDisabledPlayers") +#define SHOW_SCORE_IN_RAVE THEME->GetMetricB(m_sName,"ShowScoreInRave") +#define SONG_POSITION_METER_WIDTH THEME->GetMetricF(m_sName,"SongPositionMeterWidth") +#define PLAYER_X( sName, styleType ) THEME->GetMetricF(m_sName,ssprintf("Player%s%sX",sName.c_str(),StyleTypeToString(styleType).c_str())) +#define STOP_COURSE_EARLY THEME->GetMetricB(m_sName,"StopCourseEarly") // evaluate this every time it's used + +static ThemeMetric INITIAL_BACKGROUND_BRIGHTNESS ("ScreenGameplay","InitialBackgroundBrightness"); +static ThemeMetric SECONDS_BETWEEN_COMMENTS ("ScreenGameplay","SecondsBetweenComments"); +static ThemeMetric SCORE_KEEPER_CLASS ("ScreenGameplay","ScoreKeeperClass"); + +AutoScreenMessage( SM_PlayGo ); + +// received while STATE_DANCING +AutoScreenMessage( SM_LoadNextSong ); +AutoScreenMessage( SM_StartLoadingNextSong ); + +// received while STATE_OUTRO +AutoScreenMessage( SM_DoPrevScreen ); +AutoScreenMessage( SM_DoNextScreen ); + +// received while STATE_INTRO +AutoScreenMessage( SM_StartHereWeGo ); +AutoScreenMessage( SM_StopHereWeGo ); + +AutoScreenMessage( SM_BattleTrickLevel1 ); +AutoScreenMessage( SM_BattleTrickLevel2 ); +AutoScreenMessage( SM_BattleTrickLevel3 ); + +static Preference g_bCenter1Player( "Center1Player", false ); +static Preference g_bShowLyrics( "ShowLyrics", true ); +static Preference g_fNetStartOffset( "NetworkStartOffset", -3.0 ); +static Preference g_bEasterEggs( "EasterEggs", true ); + + +PlayerInfo::PlayerInfo(): m_pn(PLAYER_INVALID), m_mp(MultiPlayer_Invalid), + m_bIsDummy(false), m_iDummyIndex(0), m_iAddToDifficulty(0), + m_bPlayerEnabled(false), m_PlayerStateDummy(), + m_PlayerStageStatsDummy(), m_SoundEffectControl(), + m_vpStepsQueue(), m_asModifiersQueue(), m_pLifeMeter(NULL), + m_ptextCourseSongNumber(NULL), m_ptextStepsDescription(NULL), + m_pPrimaryScoreDisplay(NULL), m_pSecondaryScoreDisplay(NULL), + m_pPrimaryScoreKeeper(NULL), m_pSecondaryScoreKeeper(NULL), + m_ptextPlayerOptions(NULL), m_pActiveAttackList(NULL), + m_NoteData(), m_pPlayer(NULL), m_pInventory(NULL), + m_pStepsDisplay(NULL), m_sprOniGameOver() {} + +void PlayerInfo::Load( PlayerNumber pn, MultiPlayer mp, bool bShowNoteField, int iAddToDifficulty ) +{ + m_pn = pn; + m_mp = mp; + m_bPlayerEnabled = IsEnabled(); + m_bIsDummy = false; + m_iAddToDifficulty = iAddToDifficulty; + m_pLifeMeter = NULL; + m_ptextCourseSongNumber = NULL; + m_ptextStepsDescription = NULL; + + if( !IsMultiPlayer() ) + { + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_REGULAR: + case PLAY_MODE_NONSTOP: + case PLAY_MODE_BATTLE: + case PLAY_MODE_RAVE: + if( PREFSMAN->m_bPercentageScoring ) + m_pPrimaryScoreDisplay = new ScoreDisplayPercentage; + else + m_pPrimaryScoreDisplay = new ScoreDisplayNormal; + break; + case PLAY_MODE_ONI: + case PLAY_MODE_ENDLESS: + if( GAMESTATE->m_SongOptions.GetStage().m_LifeType == SongOptions::LIFE_TIME ) + m_pPrimaryScoreDisplay = new ScoreDisplayLifeTime; + else + m_pPrimaryScoreDisplay = new ScoreDisplayOni; + break; + default: + ASSERT(0); + } + } + + PlayerState *const pPlayerState = GetPlayerState(); + PlayerStageStats *const pPlayerStageStats = GetPlayerStageStats(); + + if( m_pPrimaryScoreDisplay ) + m_pPrimaryScoreDisplay->Init( pPlayerState, pPlayerStageStats ); + + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_RAVE: + m_pSecondaryScoreDisplay = new ScoreDisplayRave; + m_pSecondaryScoreDisplay->SetName( "ScoreDisplayRave" ); + default: + break; + } + + if( m_pSecondaryScoreDisplay ) + m_pSecondaryScoreDisplay->Init( pPlayerState, pPlayerStageStats ); + + m_pPrimaryScoreKeeper = ScoreKeeper::MakeScoreKeeper( SCORE_KEEPER_CLASS, pPlayerState, pPlayerStageStats ); + + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_RAVE: + m_pSecondaryScoreKeeper = new ScoreKeeperRave( pPlayerState, pPlayerStageStats ); + default: + break; + } + + m_ptextPlayerOptions = NULL; + m_pActiveAttackList = NULL; + m_pPlayer = new Player( m_NoteData, bShowNoteField ); + m_pInventory = NULL; + m_pStepsDisplay = NULL; + + if( IsMultiPlayer() ) + { + pPlayerState->m_PlayerOptions = GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerOptions; + } +} + +void PlayerInfo::LoadDummyP1( int iDummyIndex, int iAddToDifficulty ) +{ + m_pn = PLAYER_1; + m_bPlayerEnabled = IsEnabled(); + m_bIsDummy = true; + m_iDummyIndex = iDummyIndex; + m_iAddToDifficulty = iAddToDifficulty; + + // don't init any of the scoring objects + m_pPlayer = new Player( m_NoteData, true ); + + // PlayerOptions needs to be set now so that we load the correct NoteSkin. + m_PlayerStateDummy = *GAMESTATE->m_pPlayerState[PLAYER_1]; +} + +PlayerInfo::~PlayerInfo() +{ + SAFE_DELETE( m_pLifeMeter ); + SAFE_DELETE( m_ptextCourseSongNumber ); + SAFE_DELETE( m_ptextStepsDescription ); + SAFE_DELETE( m_pPrimaryScoreDisplay ); + SAFE_DELETE( m_pSecondaryScoreDisplay ); + SAFE_DELETE( m_pPrimaryScoreKeeper ); + SAFE_DELETE( m_pSecondaryScoreKeeper ); + SAFE_DELETE( m_ptextPlayerOptions ); + SAFE_DELETE( m_pActiveAttackList ); + SAFE_DELETE( m_pPlayer ); + SAFE_DELETE( m_pInventory ); + SAFE_DELETE( m_pStepsDisplay ); +} + +void PlayerInfo::ShowOniGameOver() +{ + m_sprOniGameOver->PlayCommand( "Die" ); +} + +PlayerState *PlayerInfo::GetPlayerState() +{ + if( m_bIsDummy ) + return &m_PlayerStateDummy; + return IsMultiPlayer() ? + GAMESTATE->m_pMultiPlayerState[ GetPlayerStateAndStageStatsIndex() ] : + GAMESTATE->m_pPlayerState[ GetPlayerStateAndStageStatsIndex() ]; +} + +PlayerStageStats *PlayerInfo::GetPlayerStageStats() +{ + // multiplayer chooses the PlayerStageStats with the highest score on StageFinalized + if( m_bIsDummy || IsMultiPlayer() ) + return &m_PlayerStageStatsDummy; + return &STATSMAN->m_CurStageStats.m_player[ GetPlayerStateAndStageStatsIndex() ]; +} + +bool PlayerInfo::IsEnabled() +{ + if( m_pn != PLAYER_INVALID ) + return GAMESTATE->IsPlayerEnabled( m_pn ); + if( m_mp != MultiPlayer_Invalid ) + return GAMESTATE->IsMultiPlayerEnabled( m_mp ); + else if( m_bIsDummy ) + return true; + ASSERT( 0 ); + return false; +} + +vector::iterator +GetNextEnabledPlayerInfo( vector::iterator iter, vector &v ) +{ + for( ; iter != v.end(); ++iter ) + { + if( !iter->m_bPlayerEnabled ) + continue; + return iter; + } + return iter; +} + +vector::iterator +GetNextEnabledPlayerInfoNotDummy( vector::iterator iter, vector &v ) +{ + for( ; iter != v.end(); iter++ ) + { + if( iter->m_bIsDummy ) + continue; + if( !iter->m_bPlayerEnabled ) + continue; + return iter; + } + return iter; +} + +vector::iterator +GetNextEnabledPlayerNumberInfo( vector::iterator iter, vector &v ) +{ + for( ; iter != v.end(); ++iter ) + { + if( iter->m_bIsDummy ) + continue; + if( !iter->m_bPlayerEnabled ) + continue; + if( iter->m_mp != MultiPlayer_Invalid ) + continue; + return iter; + } + return iter; +} + +vector::iterator +GetNextPlayerNumberInfo( vector::iterator iter, vector &v ) +{ + for( ; iter != v.end(); ++iter ) + { + if( iter->m_bIsDummy ) + continue; + if( iter->m_pn == PLAYER_INVALID ) + continue; + return iter; + } + return iter; +} + +vector::iterator +GetNextVisiblePlayerInfo( vector::iterator iter, vector &v ) +{ + for( ; iter != v.end(); ++iter ) + { + if( !iter->m_pPlayer->HasVisibleParts() ) + continue; + return iter; + } + return iter; +} + +//////////////////////////////////////////////////////////////////////////////// + +ScreenGameplay::ScreenGameplay() +{ + m_pSongBackground = NULL; + m_pSongForeground = NULL; +} + +void ScreenGameplay::Init() +{ + SubscribeToMessage( "Judgment" ); + + PLAYER_TYPE.Load( m_sName, "PlayerType" ); + PLAYER_INIT_COMMAND.Load( m_sName, "PlayerInitCommand" ); + GIVE_UP_START_TEXT.Load( m_sName, "GiveUpStartText" ); + GIVE_UP_BACK_TEXT.Load( m_sName, "GiveUpBackText" ); + GIVE_UP_ABORTED_TEXT.Load( m_sName, "GiveUpAbortedText" ); + MUSIC_FADE_OUT_SECONDS.Load( m_sName, "MusicFadeOutSeconds" ); + OUT_TRANSITION_LENGTH.Load( m_sName, "OutTransitionLength" ); + COURSE_TRANSITION_LENGTH.Load( m_sName, "CourseTransitionLength" ); + BEGIN_FAILED_DELAY.Load( m_sName, "BeginFailedDelay" ); + MIN_SECONDS_TO_STEP.Load( m_sName, "MinSecondsToStep" ); + MIN_SECONDS_TO_MUSIC.Load( m_sName, "MinSecondsToMusic" ); + MIN_SECONDS_TO_STEP_NEXT_SONG.Load( m_sName, "MinSecondsToStepNextSong" ); + START_GIVES_UP.Load( m_sName, "StartGivesUp" ); + BACK_GIVES_UP.Load( m_sName, "BackGivesUp" ); + GIVING_UP_GOES_TO_PREV_SCREEN.Load( m_sName, "GivingUpGoesToPrevScreen" ); + FAIL_ON_MISS_COMBO.Load( m_sName, "FailOnMissCombo" ); + ALLOW_CENTER_1_PLAYER.Load( m_sName, "AllowCenter1Player" ); + // configurable: + UNPAUSE_WITH_START.Load( m_sName, "UnpauseWithStart"); + SURVIVAL_MOD_OVERRIDE.Load(m_sName, "SurvivalModOverride"); + + if( UseSongBackgroundAndForeground() ) + { + m_pSongBackground = new Background; + m_pSongForeground = new Foreground; + } + + ScreenWithMenuElements::Init(); + + this->FillPlayerInfo( m_vPlayerInfo ); + + { + ASSERT_M( !m_vPlayerInfo.empty(), "m_vPlayerInfo must be filled by FillPlayerInfo" ); + + int iNumEnabledPlayers = 0; + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + ++iNumEnabledPlayers; + + /* If this is 0, we have no active players and havn't been initialized correctly. */ + ASSERT( iNumEnabledPlayers > 0 ); + } + + /* Pause MEMCARDMAN. If a memory card is removed, we don't want to interrupt the + * player by making a noise until the game finishes. */ + if( !GAMESTATE->m_bDemonstrationOrJukebox ) + MEMCARDMAN->PauseMountingThread(); + + m_pSoundMusic = NULL; + m_bPaused = false; + + m_pCombinedLifeMeter = NULL; + + if( GAMESTATE->m_pCurSong == NULL && GAMESTATE->m_pCurCourse == NULL ) + return; // ScreenDemonstration will move us to the next screen. We just need to survive for one update without crashing. + + /* Save settings to the profile now. Don't do this on extra stages, since the + * user doesn't have full control; saving would force profiles to Difficulty_Hard + * and save over their default modifiers every time someone got an extra stage. + * Do this before course modifiers are set up. */ + if( !GAMESTATE->IsAnExtraStage() ) + { + FOREACH_HumanPlayer( pn ) + GAMESTATE->SaveCurrentSettingsToProfile(pn); + } + + /* Called once per stage (single song or single course). */ + GAMESTATE->BeginStage(); + + int player = 1; + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + unsigned int count = pi->m_vpStepsQueue.size(); + + for (unsigned int i = 0; i < count; i++) + { + Steps *curSteps = pi->m_vpStepsQueue[i]; + if (curSteps->IsNoteDataEmpty()) + { + if (curSteps->GetNoteDataFromSimfile()) + { + LOG->Trace("Notes should be loaded for player %d", player); + } + else + { + LOG->Trace("Error loading notes for player %d", player); + } + } + } + player++; + } + + if(!GAMESTATE->IsCourseMode() && !GAMESTATE->m_bDemonstrationOrJukebox) + { + // fill in difficulty of CPU players with that of the first human player + // this should not need to worry about step content. + FOREACH_PotentialCpuPlayer(p) + GAMESTATE->m_pCurSteps[p].Set( GAMESTATE->m_pCurSteps[ GAMESTATE->GetFirstHumanPlayer() ] ); + + FOREACH_EnabledPlayer(p) + ASSERT( GAMESTATE->m_pCurSteps[p].Get() ); + } + + /* Increment the course play count. */ + if( GAMESTATE->IsCourseMode() && !GAMESTATE->m_bDemonstrationOrJukebox ) + FOREACH_EnabledPlayer(p) + PROFILEMAN->IncrementCoursePlayCount( GAMESTATE->m_pCurCourse, GAMESTATE->m_pCurTrail[p], p ); + + STATSMAN->m_CurStageStats.m_Stage = GAMESTATE->GetCurrentStage(); + STATSMAN->m_CurStageStats.m_iStageIndex = GAMESTATE->m_iCurrentStageIndex; + STATSMAN->m_CurStageStats.m_playMode = GAMESTATE->m_PlayMode; + STATSMAN->m_CurStageStats.m_pStyle = GAMESTATE->GetCurrentStyle(); + + /* Record combo rollover. */ + FOREACH_EnabledPlayerInfoNotDummy( m_vPlayerInfo, pi ) + pi->GetPlayerStageStats()->UpdateComboList( 0, true ); + + m_DancingState = STATE_INTRO; + + // Set this in LoadNextSong() + //m_fTimeLeftBeforeDancingComment = SECONDS_BETWEEN_COMMENTS; + + m_bZeroDeltaOnNextUpdate = false; + + + if( m_pSongBackground ) + { + m_pSongBackground->SetName( "SongBackground" ); + m_pSongBackground->SetDrawOrder( DRAW_ORDER_BEFORE_EVERYTHING ); + ActorUtil::LoadAllCommands( *m_pSongBackground, m_sName ); + this->AddChild( m_pSongBackground ); + } + + if( m_pSongForeground ) + { + m_pSongForeground->SetName( "SongForeground" ); + m_pSongForeground->SetDrawOrder( DRAW_ORDER_OVERLAY+1 ); // on top of the overlay, but under transitions + ActorUtil::LoadAllCommands( *m_pSongForeground, m_sName ); + this->AddChild( m_pSongForeground ); + } + + if( PREFSMAN->m_bShowBeginnerHelper ) + { + m_BeginnerHelper.SetDrawOrder( DRAW_ORDER_BEFORE_EVERYTHING ); + m_BeginnerHelper.SetXY( SCREEN_CENTER_X, SCREEN_CENTER_Y ); + this->AddChild( &m_BeginnerHelper ); + } + + if( !GAMESTATE->m_bDemonstrationOrJukebox ) // only load if we're going to use it + { + m_Toasty.Load( THEME->GetPathB(m_sName,"toasty") ); + this->AddChild( &m_Toasty ); + } + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + RString sName = ssprintf("Player%s", pi->GetName().c_str()); + pi->m_pPlayer->SetName( sName ); + // If pi->m_pn is set, then the player will be visible. If not, then it's not + // visible and don't bother setting its position. + + float fPlayerX; + if( GAMESTATE->m_bMultiplayer && !pi->m_bIsDummy ) + { + fPlayerX = SCREEN_CENTER_X; + } + else + { + fPlayerX = PLAYER_X( pi->GetName(), GAMESTATE->GetCurrentStyle()->m_StyleType ); + if( Center1Player() ) + fPlayerX = SCREEN_CENTER_X; + } + + pi->m_pPlayer->SetX( fPlayerX ); + pi->m_pPlayer->RunCommands( PLAYER_INIT_COMMAND ); + //ActorUtil::LoadAllCommands(pi->m_pPlayer, m_sName); + this->AddChild( pi->m_pPlayer ); + pi->m_pPlayer->PlayCommand( "On" ); + } + + FOREACH_EnabledPlayerInfoNotDummy( m_vPlayerInfo, pi ) + { + if( pi->m_pPlayer->HasVisibleParts() ) + { + pi->m_sprOniGameOver.Load( THEME->GetPathG(m_sName,"oni gameover") ); + pi->m_sprOniGameOver->SetName( ssprintf("OniGameOver%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_sprOniGameOver ); + this->AddChild( pi->m_sprOniGameOver ); + } + } + + m_NextSong.Load( THEME->GetPathB(m_sName,"next course song") ); + m_NextSong.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); + this->AddChild( &m_NextSong ); + + // Add combined life meter + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_BATTLE: + case PLAY_MODE_RAVE: + m_pCombinedLifeMeter = new CombinedLifeMeterTug; + m_pCombinedLifeMeter->SetName( "CombinedLife" ); + LOAD_ALL_COMMANDS_AND_SET_XY( *m_pCombinedLifeMeter ); + this->AddChild( m_pCombinedLifeMeter ); + default: + break; + } + + // Before the lifemeter loads, if Networking is required + // we need to wait, so that there is no Dead On Start issues. + // if you wait too long at the second checkpoint, you will + // appear dead when you begin your game. + NSMAN->StartRequest(0); + + + // Add individual life meter + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_REGULAR: + case PLAY_MODE_ONI: + case PLAY_MODE_NONSTOP: + case PLAY_MODE_ENDLESS: + FOREACH_PlayerNumberInfo( m_vPlayerInfo, pi ) + { + if( !GAMESTATE->IsPlayerEnabled(pi->m_pn) && !SHOW_LIFE_METER_FOR_DISABLED_PLAYERS ) + continue; // skip + + pi->m_pLifeMeter = LifeMeter::MakeLifeMeter( GAMESTATE->m_SongOptions.GetStage().m_LifeType ); + pi->m_pLifeMeter->Load( pi->GetPlayerState(), pi->GetPlayerStageStats() ); + pi->m_pLifeMeter->SetName( ssprintf("Life%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pLifeMeter ); + this->AddChild( pi->m_pLifeMeter ); + + // HACK: When SHOW_LIFE_METER_FOR_DISABLED_PLAYERS is enabled, + // we don't want to have any life in the disabled player's life + // meter. I think this only happens with LifeMeterBars, but I'm + // not 100% sure of that. -freem + if( !GAMESTATE->IsPlayerEnabled(pi->m_pn) && SHOW_LIFE_METER_FOR_DISABLED_PLAYERS ) + { + if(GAMESTATE->m_SongOptions.GetStage().m_LifeType == SongOptions::LIFE_BAR) + static_cast(pi->m_pLifeMeter)->ChangeLife(-1.0f); + } + } + break; + case PLAY_MODE_BATTLE: + case PLAY_MODE_RAVE: + default: + break; + } + + m_bShowScoreboard = false; + +#if !defined(WITHOUT_NETWORKING) + // Only used in SMLAN/SMOnline: + if( NSMAN->useSMserver && GAMESTATE->GetCurrentStyle()->m_StyleType != StyleType_OnePlayerTwoSides ) + { + m_bShowScoreboard = PREFSMAN->m_bEnableScoreboard.Get(); + PlayerNumber pn = GAMESTATE->GetFirstDisabledPlayer(); + if( pn != PLAYER_INVALID && m_bShowScoreboard ) + { + FOREACH_NSScoreBoardColumn( col ) + { + m_Scoreboard[col].LoadFromFont( THEME->GetPathF(m_sName,"scoreboard") ); + m_Scoreboard[col].SetShadowLength( 0 ); + m_Scoreboard[col].SetName( ssprintf("ScoreboardC%iP%i",col+1,pn+1) ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_Scoreboard[col] ); + m_Scoreboard[col].SetText( NSMAN->m_Scoreboard[col] ); + m_Scoreboard[col].SetVertAlign( align_top ); + this->AddChild( &m_Scoreboard[col] ); + } + } + } +#endif + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + // primary score display + if( pi->m_pPrimaryScoreDisplay ) + { + pi->m_pPrimaryScoreDisplay->SetName( ssprintf("Score%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pPrimaryScoreDisplay ); + if( GAMESTATE->m_PlayMode != PLAY_MODE_RAVE || SHOW_SCORE_IN_RAVE ) /* XXX: ugly */ + this->AddChild( pi->m_pPrimaryScoreDisplay ); + } + + // secondary score display + if( pi->m_pSecondaryScoreDisplay ) + { + pi->m_pSecondaryScoreDisplay->SetName( ssprintf("SecondaryScore%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pSecondaryScoreDisplay ); + this->AddChild( pi->m_pSecondaryScoreDisplay ); + } + } + + // Add stage / SongNumber + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + if( GAMESTATE->IsCourseMode() ) + { + ASSERT( pi->m_ptextCourseSongNumber == NULL ); + SONG_NUMBER_FORMAT.Load( m_sName, "SongNumberFormat" ); + pi->m_ptextCourseSongNumber = new BitmapText; + pi->m_ptextCourseSongNumber->LoadFromFont( THEME->GetPathF(m_sName,"SongNum") ); + pi->m_ptextCourseSongNumber->SetName( ssprintf("SongNumber%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_ptextCourseSongNumber ); + pi->m_ptextCourseSongNumber->SetText( "" ); + //pi->m_ptextCourseSongNumber->SetDiffuse( RageColor(0,0.5f,1,1) ); // light blue + this->AddChild( pi->m_ptextCourseSongNumber ); + } + + ASSERT( pi->m_ptextStepsDescription == NULL ); + pi->m_ptextStepsDescription = new BitmapText; + pi->m_ptextStepsDescription->LoadFromFont( THEME->GetPathF(m_sName,"StepsDescription") ); + pi->m_ptextStepsDescription->SetName( ssprintf("StepsDescription%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_ptextStepsDescription ); + this->AddChild( pi->m_ptextStepsDescription ); + + // Player/Song options + ASSERT( pi->m_ptextPlayerOptions == NULL ); + pi->m_ptextPlayerOptions = new BitmapText; + pi->m_ptextPlayerOptions->LoadFromFont( THEME->GetPathF(m_sName,"player options") ); + pi->m_ptextPlayerOptions->SetName( ssprintf("PlayerOptions%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_ptextPlayerOptions ); + this->AddChild( pi->m_ptextPlayerOptions ); + + // Difficulty icon and meter + ASSERT( pi->m_pStepsDisplay == NULL ); + pi->m_pStepsDisplay = new StepsDisplay; + pi->m_pStepsDisplay->Load("StepsDisplayGameplay", pi->GetPlayerState() ); + pi->m_pStepsDisplay->SetName( ssprintf("StepsDisplay%s",pi->GetName().c_str()) ); + PlayerNumber pn = pi->GetStepsAndTrailIndex(); + if( pn != PlayerNumber_Invalid ) + pi->m_pStepsDisplay->PlayCommand( "Set" + pi->GetName() ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pStepsDisplay ); + this->AddChild( pi->m_pStepsDisplay ); + +/* + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_BATTLE: + pi->m_pInventory = new Inventory; + pi->m_pInventory->Load( p ); + this->AddChild( pi->m_pInventory ); + break; + } +*/ + } + + m_textSongOptions.LoadFromFont( THEME->GetPathF(m_sName,"song options") ); + m_textSongOptions.SetShadowLength( 0 ); + m_textSongOptions.SetName( "SongOptions" ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_textSongOptions ); + m_textSongOptions.SetText( GAMESTATE->m_SongOptions.GetStage().GetLocalizedString() ); + this->AddChild( &m_textSongOptions ); + + FOREACH_VisiblePlayerInfo( m_vPlayerInfo, pi ) + { + ASSERT( pi->m_pActiveAttackList == NULL ); + pi->m_pActiveAttackList = new ActiveAttackList; + pi->m_pActiveAttackList->LoadFromFont( THEME->GetPathF(m_sName,"ActiveAttackList") ); + pi->m_pActiveAttackList->Init( pi->GetPlayerState() ); + pi->m_pActiveAttackList->SetName( ssprintf("ActiveAttackList%s",pi->GetName().c_str()) ); + LOAD_ALL_COMMANDS_AND_SET_XY( pi->m_pActiveAttackList ); + this->AddChild( pi->m_pActiveAttackList ); + } + + if( g_bShowLyrics ) + { + m_LyricDisplay.SetName( "LyricDisplay" ); + LOAD_ALL_COMMANDS( m_LyricDisplay ); + this->AddChild( &m_LyricDisplay ); + } + + if( !GAMESTATE->m_bDemonstrationOrJukebox ) // only load if we're going to use it + { + m_Ready.Load( THEME->GetPathB(m_sName,"ready") ); + this->AddChild( &m_Ready ); + + m_Go.Load( THEME->GetPathB(m_sName,"go") ); + this->AddChild( &m_Go ); + + m_Failed.Load( THEME->GetPathB(m_sName,"failed") ); + m_Failed.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); // on top of everything else + this->AddChild( &m_Failed ); + + m_textDebug.LoadFromFont( THEME->GetPathF(m_sName,"debug") ); + m_textDebug.SetName( "Debug" ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_textDebug ); + m_textDebug.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); // just under transitions, over the foreground + this->AddChild( &m_textDebug ); + + m_GameplayAssist.Init(); + + if( GAMESTATE->IsAnExtraStage() ) // only load if we're going to use it + { + m_textSurviveTime.LoadFromFont( THEME->GetPathF(m_sName,"survive time") ); + m_textSurviveTime.SetShadowLength( 0 ); + m_textSurviveTime.SetName( "SurviveTime" ); + LOAD_ALL_COMMANDS_AND_SET_XY( m_textSurviveTime ); + m_textSurviveTime.SetDrawOrder( DRAW_ORDER_TRANSITIONS-1 ); + m_textSurviveTime.SetDiffuse( RageColor(1,1,1,0) ); + this->AddChild( &m_textSurviveTime ); + } + + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_BATTLE: + m_soundBattleTrickLevel1.Load( THEME->GetPathS(m_sName,"battle trick level1"), true ); + m_soundBattleTrickLevel2.Load( THEME->GetPathS(m_sName,"battle trick level2"), true ); + m_soundBattleTrickLevel3.Load( THEME->GetPathS(m_sName,"battle trick level3"), true ); + default: + break; + } + } + + if( m_pSongBackground ) + m_pSongBackground->Init(); + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + RString sType = PLAYER_TYPE; + if( pi->m_bIsDummy ) + sType += "Dummy"; + pi->m_pPlayer->Init( + sType, + pi->GetPlayerState(), + pi->GetPlayerStageStats(), + pi->m_pLifeMeter, + m_pCombinedLifeMeter, + pi->m_pPrimaryScoreDisplay, + pi->m_pSecondaryScoreDisplay, + pi->m_pInventory, + pi->m_pPrimaryScoreKeeper, + pi->m_pSecondaryScoreKeeper ); + } + + // fill in m_apSongsQueue, m_vpStepsQueue, m_asModifiersQueue + InitSongQueues(); + + // Fill StageStats + STATSMAN->m_CurStageStats.m_vpPossibleSongs = m_apSongsQueue; + FOREACH( PlayerInfo, m_vPlayerInfo, pi ) + { + if( pi->GetPlayerStageStats() ) + pi->GetPlayerStageStats()->m_vpPossibleSteps = pi->m_vpStepsQueue; + } + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + ASSERT( !pi->m_vpStepsQueue.empty() ); + if( pi->GetPlayerStageStats() ) + pi->GetPlayerStageStats()->m_bJoined = true; + if( pi->m_pPrimaryScoreKeeper ) + pi->m_pPrimaryScoreKeeper->Load( m_apSongsQueue, pi->m_vpStepsQueue, pi->m_asModifiersQueue ); + if( pi->m_pSecondaryScoreKeeper ) + pi->m_pSecondaryScoreKeeper->Load( m_apSongsQueue, pi->m_vpStepsQueue, pi->m_asModifiersQueue ); + } + + GAMESTATE->m_bGameplayLeadIn.Set( true ); + + /* LoadNextSong first, since that positions some elements which need to be + * positioned before we TweenOnScreen. */ + LoadNextSong(); + + m_GiveUpTimer.SetZero(); +} + +bool ScreenGameplay::Center1Player() const +{ + /* Perhaps this should be handled better by defining a new + * StyleType for ONE_PLAYER_ONE_CREDIT_AND_ONE_COMPUTER, + * but for now just ignore Center1Player when it's Battle or Rave + * Mode. This doesn't begin to address two-player solo (6 arrows) */ + return g_bCenter1Player && + (bool)ALLOW_CENTER_1_PLAYER && + GAMESTATE->m_PlayMode != PLAY_MODE_BATTLE && + GAMESTATE->m_PlayMode != PLAY_MODE_RAVE && + GAMESTATE->GetCurrentStyle()->m_StyleType == StyleType_OnePlayerOneSide; +} + +// fill in m_apSongsQueue, m_vpStepsQueue, m_asModifiersQueue +void ScreenGameplay::InitSongQueues() +{ + if( GAMESTATE->IsCourseMode() ) + { + Course* pCourse = GAMESTATE->m_pCurCourse; + ASSERT( pCourse ); + + m_apSongsQueue.clear(); + PlayerNumber pnMaster = GAMESTATE->GetMasterPlayerNumber(); + Trail *pTrail = GAMESTATE->m_pCurTrail[pnMaster]; + ASSERT( pTrail ); + FOREACH_CONST( TrailEntry, pTrail->m_vEntries, e ) + { + ASSERT( e->pSong ); + m_apSongsQueue.push_back( e->pSong ); + } + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + Trail *lTrail = GAMESTATE->m_pCurTrail[ pi->GetStepsAndTrailIndex() ]; + ASSERT( lTrail ); + + pi->m_vpStepsQueue.clear(); + pi->m_asModifiersQueue.clear(); + FOREACH_CONST( TrailEntry, lTrail->m_vEntries, e ) + { + ASSERT( e->pSteps ); + pi->m_vpStepsQueue.push_back( e->pSteps ); + AttackArray a; + e->GetAttackArray( a ); + pi->m_asModifiersQueue.push_back( a ); + } + + // In a survival course, override stored mods + if( pCourse->GetCourseType() == COURSE_TYPE_SURVIVAL && SURVIVAL_MOD_OVERRIDE ) + { + pi->GetPlayerState()->m_PlayerOptions.FromString( ModsLevel_Stage, + "clearall," + + CommonMetrics::DEFAULT_NOTESKIN_NAME.GetValue() + + "," + + CommonMetrics::DEFAULT_MODIFIERS.GetValue() ); + pi->GetPlayerState()->RebuildPlayerOptionsFromActiveAttacks(); + } + } + } + else + { + m_apSongsQueue.push_back( GAMESTATE->m_pCurSong ); + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + Steps *pSteps = GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ]; + pi->m_vpStepsQueue.push_back( pSteps ); + const PlayerOptions &p = pi->GetPlayerState()->m_PlayerOptions.GetCurrent(); + + if (p.m_fNoAttack == 0 && p.m_fRandAttack == 0 && + pSteps->m_Attacks.size() > 0 ) + { + pi->m_asModifiersQueue.push_back( pSteps->m_Attacks ); + } + else + { + AttackArray aa; + pi->m_asModifiersQueue.push_back( aa ); + } + } + } + + if( GAMESTATE->m_bMultiplayer ) + { + for( int i=0; i<(int)m_apSongsQueue.size(); i++ ) + { + Song *pSong = m_apSongsQueue[i]; + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + Steps *pOldSteps = pi->m_vpStepsQueue[i]; + + vector vpSteps; + SongUtil::GetSteps( pSong, vpSteps, pOldSteps->m_StepsType ); + StepsUtil::SortNotesArrayByDifficulty( vpSteps ); + vector::iterator iter = find( vpSteps.begin(), vpSteps.end(), pOldSteps ); + int iIndexBase = 0; + if( iter != vpSteps.end() ) + { + iIndexBase = iter - vpSteps.begin(); + CLAMP( iIndexBase, 0, vpSteps.size() - GAMESTATE->m_iNumMultiplayerNoteFields ); + } + + int iIndexToUse = iIndexBase + pi->m_iAddToDifficulty; + CLAMP( iIndexToUse, 0, vpSteps.size()-1 ); + + Steps *pSteps = vpSteps[iIndexToUse]; + pi->m_vpStepsQueue[i] = pSteps; + } + } + } +} + +ScreenGameplay::~ScreenGameplay() +{ + if( this->IsFirstUpdate() ) + { + /* We never received any updates. That means we were deleted without being + * used, and never actually played. (This can happen when backing out of + * ScreenStage.) Cancel the stage. */ + GAMESTATE->CancelStage(); + } + + LOG->Trace( "ScreenGameplay::~ScreenGameplay()" ); + + SAFE_DELETE( m_pSongBackground ); + SAFE_DELETE( m_pSongForeground ); + + if( !GAMESTATE->m_bDemonstrationOrJukebox ) + MEMCARDMAN->UnPauseMountingThread(); + + SAFE_DELETE( m_pCombinedLifeMeter ); + if( m_pSoundMusic ) + m_pSoundMusic->StopPlaying(); + + m_GameplayAssist.StopPlaying(); + + NSMAN->ReportSongOver(); +} + +bool ScreenGameplay::IsLastSong() +{ + if( GAMESTATE->m_pCurCourse ) + { + if( GAMESTATE->m_pCurCourse->m_fGoalSeconds > 0 && GAMESTATE->m_bWorkoutGoalComplete ) + return true; + } + if( GAMESTATE->m_pCurCourse && GAMESTATE->m_pCurCourse->m_bRepeat ) + return false; + return GAMESTATE->GetCourseSongIndex() >= (int)m_apSongsQueue.size()-1; // GetCourseSongIndex() is 0-based +} + +void ScreenGameplay::SetupSong( int iSongIndex ) +{ + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + /* This is the first beat that can be changed without it being visible. + * Until we draw for the first time, any beat can be changed. */ + pi->GetPlayerState()->m_fLastDrawnBeat = -100; + + Steps *pSteps = pi->m_vpStepsQueue[iSongIndex]; + GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ].Set( pSteps ); + + /* Load new NoteData into Player. Do this before + * RebuildPlayerOptionsFromActiveAttacks or else transform mods will get + * propagated to GAMESTATE->m_pPlayerOptions too early and be double-applied + * to the NoteData: + * once in Player::Load, then again in Player::ApplyActiveAttacks. + * This is very bad for transforms like AddMines. + */ + NoteData originalNoteData; + pSteps->GetNoteData( originalNoteData ); + + const Style* pStyle = GAMESTATE->GetCurrentStyle(); + NoteData ndTransformed; + pStyle->GetTransformedNoteDataForStyle( pi->GetStepsAndTrailIndex(), originalNoteData, ndTransformed ); + + // HACK: Apply NoteSkins from global course options. Do this before + // Player::Load, since it needs to know which note skin to load. + pi->GetPlayerState()->m_ModsToApply.clear(); + for( unsigned i=0; im_asModifiersQueue[iSongIndex].size(); ++i ) + { + Attack a = pi->m_asModifiersQueue[iSongIndex][i]; + if( a.fStartSecond != 0 ) + continue; + a.fStartSecond = a.ATTACK_STARTS_NOW; // now + + PlayerOptions po; + po.FromString( a.sModifiers ); + if( po.m_sNoteSkin.empty() ) + continue; + a.sModifiers = po.m_sNoteSkin; + + pi->GetPlayerState()->LaunchAttack( a ); + } + + /* Update attack bOn flags, and rebuild Current-level options + * from Song-level options. The current NoteSkin could have changed + * because of an attack ending. */ + pi->GetPlayerState()->Update( 0 ); + + // load player + { + pi->m_NoteData = ndTransformed; + NoteDataUtil::RemoveAllTapsOfType( pi->m_NoteData, TapNote::autoKeysound ); + pi->m_pPlayer->Load(); + } + + // load auto keysounds + { + NoteData nd = ndTransformed; + NoteDataUtil::RemoveAllTapsExceptForType( nd, TapNote::autoKeysound ); + m_AutoKeysounds.Load( pi->GetStepsAndTrailIndex(), nd ); + } + + { + RString sType; + switch( GAMESTATE->m_SongOptions.GetCurrent().m_SoundEffectType ) + { + case SongOptions::SOUNDEFFECT_OFF: sType = "SoundEffectControl_Off"; break; + case SongOptions::SOUNDEFFECT_SPEED: sType = "SoundEffectControl_Speed"; break; + case SongOptions::SOUNDEFFECT_PITCH: sType = "SoundEffectControl_Pitch"; break; + default: break; + } + + pi->m_SoundEffectControl.Load( sType, pi->GetPlayerState(), &pi->m_NoteData ); + } + + // Put course options into effect. Do this after Player::Load so + // that mods aren't double-applied. + pi->GetPlayerState()->m_ModsToApply.clear(); + for( unsigned i=0; im_asModifiersQueue[iSongIndex].size(); ++i ) + { + Attack a = pi->m_asModifiersQueue[iSongIndex][i]; + if( a.fStartSecond == 0 ) + a.fStartSecond = a.ATTACK_STARTS_NOW; // now + + pi->GetPlayerState()->LaunchAttack( a ); + GAMESTATE->m_SongOptions.FromString( ModsLevel_Song, a.sModifiers ); + } + + // Update attack bOn flags. + pi->GetPlayerState()->Update( 0 ); + + // Hack: Course modifiers that are set to start immediately shouldn't tween on. + pi->GetPlayerState()->m_PlayerOptions.SetCurrentToLevel( ModsLevel_Stage ); + } +} + +void ScreenGameplay::ReloadCurrentSong() +{ + FOREACH_EnabledPlayerInfoNotDummy( m_vPlayerInfo, pi ) + pi->GetPlayerStageStats()->m_iSongsPlayed--; + + LoadNextSong(); +} + +void ScreenGameplay::LoadNextSong() +{ + GAMESTATE->ResetMusicStatistics(); + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + pi->GetPlayerStageStats()->m_iSongsPlayed++; + if( pi->m_ptextCourseSongNumber ) + pi->m_ptextCourseSongNumber->SetText( ssprintf(SONG_NUMBER_FORMAT.GetValue(), pi->GetPlayerStageStats()->m_iSongsPassed+1) ); + } + + if( GAMESTATE->m_bMultiplayer ) + { + FOREACH_ENUM( MultiPlayer, mp ) + this->UpdateStageStats( mp ); + } + + int iPlaySongIndex = GAMESTATE->GetCourseSongIndex(); + iPlaySongIndex %= m_apSongsQueue.size(); + GAMESTATE->m_pCurSong.Set( m_apSongsQueue[iPlaySongIndex] ); + STATSMAN->m_CurStageStats.m_vpPlayedSongs.push_back( GAMESTATE->m_pCurSong ); + + // No need to do this here. We do it in SongFinished(). + //GAMESTATE->RemoveAllActiveAttacks(); + + /* If we're in battery mode, force FailImmediate. We assume in Player::Step + * that failed players can't step. */ + if( GAMESTATE->m_SongOptions.GetCurrent().m_LifeType == SongOptions::LIFE_BATTERY ) + { + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + PO_GROUP_ASSIGN( pi->GetPlayerState()->m_PlayerOptions, ModsLevel_Song, m_FailType, PlayerOptions::FAIL_IMMEDIATE ); + } + + m_textSongOptions.SetText( GAMESTATE->m_SongOptions.GetCurrent().GetString() ); + + SetupSong( iPlaySongIndex ); + + Song* pSong = GAMESTATE->m_pCurSong; + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + Steps* pSteps = GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ]; + ++pi->GetPlayerStageStats()->m_iStepsPlayed; + + ASSERT( GAMESTATE->m_pCurSteps[ pi->GetStepsAndTrailIndex() ] ); + if( pi->m_ptextStepsDescription ) + pi->m_ptextStepsDescription->SetText( pSteps->GetDescription() ); + + /* Increment the play count even if the player fails. (It's still popular, + * even if the people playing it aren't good at it.) */ + if( !GAMESTATE->m_bDemonstrationOrJukebox ) + { + if( pi->m_pn != PLAYER_INVALID ) + PROFILEMAN->IncrementStepsPlayCount( pSong, pSteps, pi->m_pn ); + } + + if( pi->m_ptextPlayerOptions ) + pi->m_ptextPlayerOptions->SetText( pi->GetPlayerState()->m_PlayerOptions.GetCurrent().GetString() ); + if( pi->m_pActiveAttackList ) + pi->m_pActiveAttackList->Refresh(); + + // reset oni game over graphic + SET_XY_AND_ON_COMMAND( pi->m_sprOniGameOver ); + + if( GAMESTATE->m_SongOptions.GetCurrent().m_LifeType==SongOptions::LIFE_BATTERY && pi->GetPlayerStageStats()->m_bFailed ) // already failed + pi->ShowOniGameOver(); + + if( GAMESTATE->m_SongOptions.GetCurrent().m_LifeType==SongOptions::LIFE_BAR && pi->m_pLifeMeter ) + pi->m_pLifeMeter->UpdateNonstopLifebar(); + + if( pi->m_pStepsDisplay ) + pi->m_pStepsDisplay->SetFromSteps( pSteps ); + + /* The actual note data for scoring is the base class of Player. This includes + * transforms, like Wide. Otherwise, the scoring will operate on the wrong data. */ + if( pi->m_pPrimaryScoreKeeper ) + pi->m_pPrimaryScoreKeeper->OnNextSong( GAMESTATE->GetCourseSongIndex(), pSteps, &pi->m_pPlayer->GetNoteData() ); + if( pi->m_pSecondaryScoreKeeper ) + pi->m_pSecondaryScoreKeeper->OnNextSong( GAMESTATE->GetCourseSongIndex(), pSteps, &pi->m_pPlayer->GetNoteData() ); + + // Don't mess with the PlayerController of the Dummy player + if( !pi->m_bIsDummy ) + { + if( GAMESTATE->m_bDemonstrationOrJukebox ) + { + pi->GetPlayerState()->m_PlayerController = PC_CPU; + pi->GetPlayerState()->m_iCpuSkill = 5; + } + else if( GAMESTATE->IsCpuPlayer(pi->GetStepsAndTrailIndex()) ) + { + pi->GetPlayerState()->m_PlayerController = PC_CPU; + int iMeter = pSteps->GetMeter(); + int iNewSkill = SCALE( iMeter, MIN_METER, MAX_METER, 0, NUM_SKILL_LEVELS-1 ); + /* Watch out: songs aren't actually bound by MAX_METER. */ + iNewSkill = clamp( iNewSkill, 0, NUM_SKILL_LEVELS-1 ); + pi->GetPlayerState()->m_iCpuSkill = iNewSkill; + } + else + { + if( pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_fPlayerAutoPlay != 0 ) + pi->GetPlayerState()->m_PlayerController = PC_AUTOPLAY; + else + pi->GetPlayerState()->m_PlayerController = GamePreferences::m_AutoPlay; + } + } + } + + bool bAllReverse = true; + bool bAtLeastOneReverse = false; + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_fScrolls[PlayerOptions::SCROLL_REVERSE] == 1 ) + bAtLeastOneReverse = true; + else + bAllReverse = false; + } + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + bool bReverse = pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_fScrolls[PlayerOptions::SCROLL_REVERSE] == 1; + + if( pi->m_pStepsDisplay ) + pi->m_pStepsDisplay->PlayCommand( bReverse? "SetReverse":"SetNoReverse" ); + } + + m_LyricDisplay.PlayCommand( bAllReverse? "SetReverse": bAtLeastOneReverse? "SetOneReverse": "SetNoReverse" ); + + // Load lyrics + // XXX: don't load this here (who and why? -aj) + LyricsLoader LL; + if( GAMESTATE->m_pCurSong->HasLyrics() ) + LL.LoadFromLRCFile(GAMESTATE->m_pCurSong->GetLyricsPath(), *GAMESTATE->m_pCurSong); + + // Set up song-specific graphics. + + // Check to see if any players are in beginner mode. + // Note: steps can be different if turn modifiers are used. + if( PREFSMAN->m_bShowBeginnerHelper ) + { + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + PlayerNumber pn = pi->GetStepsAndTrailIndex(); + if( GAMESTATE->IsHumanPlayer(pn) && GAMESTATE->m_pCurSteps[pn]->GetDifficulty() == Difficulty_Beginner ) + m_BeginnerHelper.AddPlayer( pn, pi->m_pPlayer->GetNoteData() ); + } + } + + if( m_pSongBackground ) + m_pSongBackground->Unload(); + + if( m_pSongForeground ) + m_pSongForeground->Unload(); + + if( !PREFSMAN->m_bShowBeginnerHelper || !m_BeginnerHelper.Init(2) ) + { + m_BeginnerHelper.SetVisible( false ); + + // BeginnerHelper disabled, or failed to load. + if( m_pSongBackground ) + m_pSongBackground->LoadFromSong( GAMESTATE->m_pCurSong ); + + if( !GAMESTATE->m_bDemonstrationOrJukebox ) + { + /* This will fade from a preset brightness to the actual brightness + * (based on prefs and "cover"). The preset brightness may be 0 (to + * fade from black), or it might be 1, if the stage screen has the + * song BG and we're coming from it (like Pump). This used to be done + * in SM_PlayReady, but that means it's impossible to snap to the + * new brightness immediately. */ + if( m_pSongBackground ) + { + m_pSongBackground->SetBrightness( INITIAL_BACKGROUND_BRIGHTNESS ); + m_pSongBackground->FadeToActualBrightness(); + } + } + } + else + { + m_BeginnerHelper.SetVisible( true ); + } + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( !pi->GetPlayerStageStats()->m_bFailed ) + { + // give a little life back between stages + if( pi->m_pLifeMeter ) + pi->m_pLifeMeter->OnLoadSong(); + if( pi->m_pPrimaryScoreDisplay ) + pi->m_pPrimaryScoreDisplay->OnLoadSong(); + if( pi->m_pSecondaryScoreDisplay ) + pi->m_pSecondaryScoreDisplay->OnLoadSong(); + } + } + if( m_pCombinedLifeMeter ) + m_pCombinedLifeMeter->OnLoadSong(); + + if( m_pSongForeground ) + m_pSongForeground->LoadFromSong( GAMESTATE->m_pCurSong ); + + m_fTimeSinceLastDancingComment = 0; + + /* m_soundMusic and m_pSongBackground take a very long time to load, + * so cap fDelta at 0 so m_NextSong will show up on screen. + * -Chris */ + m_bZeroDeltaOnNextUpdate = true; + SCREENMAN->ZeroNextUpdate(); + + // Load cabinet lights data + LoadLights(); + + /* Load the music last, since it may start streaming and we don't want the music + * to compete with other loading. */ + m_AutoKeysounds.FinishLoading(); + m_pSoundMusic = m_AutoKeysounds.GetSound(); + + /* Give SoundEffectControls the new RageSoundReaders. */ + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + RageSoundReader *pPlayerSound = m_AutoKeysounds.GetPlayerSound(pi->m_pn); + if( pPlayerSound == NULL && pi->m_pn == GAMESTATE->GetMasterPlayerNumber() ) + pPlayerSound = m_AutoKeysounds.GetSharedSound(); + pi->m_SoundEffectControl.SetSoundReader( pPlayerSound ); + } + + MESSAGEMAN->Broadcast("DoneLoadingNextSong"); +} + +void ScreenGameplay::LoadLights() +{ + if( !LIGHTSMAN->IsEnabled() ) + return; + + // First, check if the song has explicit lights + m_CabinetLightsNoteData.Init(); + ASSERT( GAMESTATE->m_pCurSong ); + + const Steps *pSteps = SongUtil::GetClosestNotes( GAMESTATE->m_pCurSong, StepsType_lights_cabinet, Difficulty_Medium ); + if( pSteps != NULL ) + { + pSteps->GetNoteData( m_CabinetLightsNoteData ); + return; + } + + // No explicit lights. Create autogen lights. + RString sDifficulty = PREFSMAN->m_sLightsStepsDifficulty; + vector asDifficulties; + split( sDifficulty, ",", asDifficulties ); + + // Always use the steps from the primary steps type so that lights are consistent over single and double styles. + StepsType st = GAMEMAN->GetHowToPlayStyleForGame( GAMESTATE->m_pCurGame )->m_StepsType; + + Difficulty d1 = Difficulty_Invalid; + if( asDifficulties.size() > 0 ) + { + if( asDifficulties[0].CompareNoCase("selected") == 0 ) + { + // Base lights off current difficulty of active player + // Can be either P1 or P2 if they're individual or P1 if both are active + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + PlayerNumber pn = pi->GetStepsAndTrailIndex(); + + if( GAMESTATE->IsPlayerEnabled(pn) ) + { + d1 = GAMESTATE->m_pCurSteps[pn]->GetDifficulty(); + break; + } + } + } + else + d1 = StringToDifficulty( asDifficulties[0] ); + } + + pSteps = SongUtil::GetClosestNotes( GAMESTATE->m_pCurSong, st, d1 ); + + // If we can't find anything at all, stop. + if( pSteps == NULL ) + return; + + NoteData TapNoteData1; + pSteps->GetNoteData( TapNoteData1 ); + + if( asDifficulties.size() > 1 ) + { + Difficulty d2 = Difficulty_Invalid; + + // We've also specified for Player 2 to be based off current difficulty + if( asDifficulties[1].CompareNoCase("selected") == 0 && GAMESTATE->GetNumPlayersEnabled() > 1 ) + { + // Base lights off current difficulty of active player + // Only do this for P2 in a two-player situation, since P1 is taken care of above + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + PlayerNumber pn = pi->GetStepsAndTrailIndex(); + + if( pn == PLAYER_2 ) + d2 = GAMESTATE->m_pCurSteps[pn]->GetDifficulty(); + } + } + else + d2 = StringToDifficulty( asDifficulties[1] ); + + /* fall through */ + } + + NoteDataUtil::LoadTransformedLights( TapNoteData1, m_CabinetLightsNoteData, GAMEMAN->GetStepsTypeInfo(StepsType_lights_cabinet).iNumTracks ); +} + +void ScreenGameplay::StartPlayingSong( float fMinTimeToNotes, float fMinTimeToMusic ) +{ + ASSERT( fMinTimeToNotes >= 0 ); + ASSERT( fMinTimeToMusic >= 0 ); + + m_pSoundMusic->SetProperty( "AccurateSync", true ); + + RageSoundParams p; + p.m_fSpeed = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; + p.StopMode = RageSoundParams::M_CONTINUE; + + { + const float fFirstSecond = GAMESTATE->m_pCurSong->GetFirstSecond(); + float fStartDelay = fMinTimeToNotes - fFirstSecond; + fStartDelay = max( fStartDelay, fMinTimeToMusic ); + p.m_StartSecond = -fStartDelay; + } + + ASSERT( !m_pSoundMusic->IsPlaying() ); + { + float fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut; + GetMusicEndTiming( fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut ); + + if( fSecondsToStartFadingOutMusic < GAMESTATE->m_pCurSong->m_fMusicLengthSeconds ) + { + p.m_fFadeOutSeconds = MUSIC_FADE_OUT_SECONDS; + p.m_LengthSeconds = fSecondsToStartFadingOutMusic + MUSIC_FADE_OUT_SECONDS - p.m_StartSecond; + } + } + m_pSoundMusic->Play( &p ); + if( m_bPaused ) + m_pSoundMusic->Pause( true ); + + /* Make sure GAMESTATE->m_fMusicSeconds is set up. */ + GAMESTATE->m_Position.m_fMusicSeconds = -5000; + UpdateSongPosition(0); + + ASSERT( GAMESTATE->m_Position.m_fMusicSeconds > -4000 ); /* make sure the "fake timer" code doesn't trigger */ +} + + +void ScreenGameplay::PauseGame( bool bPause, GameController gc ) +{ + if( m_bPaused == bPause ) + { + LOG->Trace( "ScreenGameplay::PauseGame(%i) received, but already in that state; ignored", bPause ); + return; + } + + // Don't pause if we're already tweening out. + if( bPause && m_DancingState == STATE_OUTRO ) + return; + + AbortGiveUp( false ); + + m_bPaused = bPause; + m_PauseController = gc; + + m_pSoundMusic->Pause( bPause ); + if( bPause ) + this->PlayCommand( "Pause" ); + else + this->PlayCommand( "Unpause" ); + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + pi->m_pPlayer->SetPaused( m_bPaused ); + } +} + +// play assist ticks +void ScreenGameplay::PlayTicks() +{ + /* TODO: Allow all players to have ticks. Not as simple as it looks. + * If a loop takes place, it could make one player's ticks come later + * than intended. Any help here would be appreciated. -Wolfman2000 */ + Player &player = *m_vPlayerInfo[GAMESTATE->GetMasterPlayerNumber()].m_pPlayer; + const NoteData &nd = player.GetNoteData(); + m_GameplayAssist.PlayTicks( nd, player.GetPlayerState() ); +} + +/* Play announcer "type" if it's been at least fSeconds since the last announcer. */ +void ScreenGameplay::PlayAnnouncer( const RString &type, float fSeconds, float *fDeltaSeconds ) +{ + if( GAMESTATE->m_fOpponentHealthPercent == 0 ) + return; // Shut the announcer up + + /* Don't play in demonstration. */ + if( GAMESTATE->m_bDemonstrationOrJukebox ) + return; + + /* Don't play before the first beat, or after we're finished. */ + if( m_DancingState != STATE_DANCING ) + return; + if(GAMESTATE->m_pCurSong == NULL || // this will be true on ScreenDemonstration sometimes + GAMESTATE->m_Position.m_fSongBeat < GAMESTATE->m_pCurSong->GetFirstBeat()) + return; + + if( *fDeltaSeconds < fSeconds ) + return; + *fDeltaSeconds = 0; + + SOUND->PlayOnceFromAnnouncer( type ); +} + +void ScreenGameplay::UpdateSongPosition( float fDeltaTime ) +{ + if( !m_pSoundMusic->IsPlaying() ) + return; + + RageTimer tm; + const float fSeconds = m_pSoundMusic->GetPositionSeconds( NULL, &tm ); + const float fAdjust = SOUND->GetFrameTimingAdjustment( fDeltaTime ); + GAMESTATE->UpdateSongPosition( fSeconds+fAdjust, GAMESTATE->m_pCurSong->m_SongTiming, tm+fAdjust, true ); +} + +void ScreenGameplay::BeginScreen() +{ + if( GAMESTATE->m_pCurSong == NULL ) + return; + + ScreenWithMenuElements::BeginScreen(); + + SOUND->PlayOnceFromAnnouncer( "gameplay intro" ); // crowd cheer + + // Get the transitions rolling + if( GAMESTATE->m_bDemonstrationOrJukebox ) + { + StartPlayingSong( MIN_SECONDS_TO_STEP, MIN_SECONDS_TO_MUSIC ); + } + else if( NSMAN->useSMserver ) + { + // If we're using networking, we must not have any delay. If we do, + // this can cause inconsistency on different computers and + // different themes. + + StartPlayingSong( 0, 0 ); + m_pSoundMusic->Stop(); + + float startOffset = g_fNetStartOffset; + + NSMAN->StartRequest(1); + + RageSoundParams p; + p.m_fSpeed = 1.0f; // Force 1.0 playback speed + p.StopMode = RageSoundParams::M_CONTINUE; + p.m_StartSecond = startOffset; + m_pSoundMusic->SetProperty( "AccurateSync", true ); + m_pSoundMusic->Play( &p ); + + UpdateSongPosition(0); + } + else + { + StartPlayingSong( MIN_SECONDS_TO_STEP, MIN_SECONDS_TO_MUSIC ); + } +} + +bool ScreenGameplay::AllAreFailing() +{ + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( pi->m_pLifeMeter && !pi->m_pLifeMeter->IsFailing() ) + return false; + } + return true; +} + +void ScreenGameplay::GetMusicEndTiming( float &fSecondsToStartFadingOutMusic, float &fSecondsToStartTransitioningOut ) +{ + float fLastStepSeconds = GAMESTATE->m_pCurSong->GetLastSecond(); + fLastStepSeconds += Player::GetMaxStepDistanceSeconds(); + + float fTransitionLength; + if( !GAMESTATE->IsCourseMode() || IsLastSong() ) + fTransitionLength = OUT_TRANSITION_LENGTH; + else + fTransitionLength = COURSE_TRANSITION_LENGTH; + + fSecondsToStartTransitioningOut = fLastStepSeconds; + + // Align the end of the music fade to the end of the transition. + float fSecondsToFinishFadingOutMusic = fSecondsToStartTransitioningOut + fTransitionLength; + if( fSecondsToFinishFadingOutMusic < GAMESTATE->m_pCurSong->m_fMusicLengthSeconds ) + fSecondsToStartFadingOutMusic = fSecondsToFinishFadingOutMusic - MUSIC_FADE_OUT_SECONDS; + else + fSecondsToStartFadingOutMusic = GAMESTATE->m_pCurSong->m_fMusicLengthSeconds; // don't fade + + /* Make sure we keep going long enough to register a miss for the last note, and + * never start fading before the last note. */ + fSecondsToStartFadingOutMusic = max( fSecondsToStartFadingOutMusic, fLastStepSeconds ); + fSecondsToStartTransitioningOut = max( fSecondsToStartTransitioningOut, fLastStepSeconds ); + + /* Make sure the fade finishes before the transition finishes. */ + fSecondsToStartTransitioningOut = max( fSecondsToStartTransitioningOut, fSecondsToStartFadingOutMusic + MUSIC_FADE_OUT_SECONDS - fTransitionLength ); +} + +void ScreenGameplay::Update( float fDeltaTime ) +{ + if( GAMESTATE->m_pCurSong == NULL ) + { + /* ScreenDemonstration will move us to the next screen. We just need to + * survive for one update without crashing. We need to call Screen::Update + * to make sure we receive the next-screen message. */ + Screen::Update( fDeltaTime ); + return; + } + + UpdateSongPosition( fDeltaTime ); + + if( m_bZeroDeltaOnNextUpdate ) + { + Screen::Update( 0 ); + m_bZeroDeltaOnNextUpdate = false; + } + else + { + Screen::Update( fDeltaTime ); + } + + /* This happens if ScreenDemonstration::HandleScreenMessage sets a new screen when + * PREFSMAN->m_bDelayedScreenLoad. */ + if( GAMESTATE->m_pCurSong == NULL ) + return; + /* This can happen if ScreenDemonstration::HandleScreenMessage sets a new screen when + * !PREFSMAN->m_bDelayedScreenLoad. (The new screen was loaded when we called Screen::Update, + * and the ctor might set a new GAMESTATE->m_pCurSong, so the above check can fail.) */ + if( SCREENMAN->GetTopScreen() != this ) + return; + + /* Update actors when paused, but never move on to another state. */ + if( m_bPaused ) + return; + + //LOG->Trace( "m_fOffsetInBeats = %f, m_fBeatsPerSecond = %f, m_Music.GetPositionSeconds = %f", m_fOffsetInBeats, m_fBeatsPerSecond, m_Music.GetPositionSeconds() ); + + m_AutoKeysounds.Update(fDeltaTime); + + // update GameState HealthState + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + HealthState &hs = pi->GetPlayerState()->m_HealthState; + HealthState OldHealthState = hs; + if( GAMESTATE->GetPlayerFailType(pi->GetPlayerState()) != PlayerOptions::FAIL_OFF && + pi->m_pLifeMeter && pi->m_pLifeMeter->IsFailing() ) + { + hs = HealthState_Dead; + } + else if( pi->m_pLifeMeter && pi->m_pLifeMeter->IsHot() ) + { + hs = HealthState_Hot; + } + else if( GAMESTATE->GetPlayerFailType(pi->GetPlayerState()) != PlayerOptions::FAIL_OFF && + pi->m_pLifeMeter && pi->m_pLifeMeter->IsInDanger() ) + { + hs = HealthState_Danger; + } + else + { + hs = HealthState_Alive; + } + + if( hs != OldHealthState ) + { + Message msg( "HealthStateChanged" ); + msg.SetParam( "PlayerNumber", pi->m_pn ); + msg.SetParam( "HealthState", hs ); + msg.SetParam( "OldHealthState", OldHealthState ); + MESSAGEMAN->Broadcast( msg ); + } + + pi->m_SoundEffectControl.Update( fDeltaTime ); + } + + { + float fSpeed = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; + if( GAMESTATE->m_SongOptions.GetCurrent().m_fHaste != 0.0f ) + fSpeed *= GetHasteRate(); + + RageSoundParams p = m_pSoundMusic->GetParams(); + if( fabsf(p.m_fSpeed - fSpeed) > 0.01f ) + { + p.m_fSpeed = fSpeed; + m_pSoundMusic->SetParams( p ); + } + } + + switch( m_DancingState ) + { + case STATE_DANCING: + { + /* Set STATSMAN->m_CurStageStats.bFailed for failed players. In, FAIL_IMMEDIATE, send + * SM_BeginFailed if all players failed, and kill dead Oni players. */ + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + PlayerNumber pn = pi->GetStepsAndTrailIndex(); + + PlayerOptions::FailType ft = GAMESTATE->GetPlayerFailType( pi->GetPlayerState() ); + SongOptions::LifeType lt = GAMESTATE->m_SongOptions.GetCurrent().m_LifeType; + + if( ft == PlayerOptions::FAIL_OFF || ft == PlayerOptions::FAIL_AT_END ) + continue; + + // check for individual fail + if( pi->m_pLifeMeter == NULL || !pi->m_pLifeMeter->IsFailing() ) + continue; /* isn't failing */ + if( pi->GetPlayerStageStats()->m_bFailed ) + continue; /* failed and is already dead */ + + LOG->Trace("Player %d failed", (int)pn); + pi->GetPlayerStageStats()->m_bFailed = true; // fail + + { + Message msg("PlayerFailed"); + msg.SetParam( "PlayerNumber", pi->m_pn ); + MESSAGEMAN->Broadcast( msg ); + } + + // Check for and do Oni die. + bool bAllowOniDie = false; + switch( lt ) + { + case SongOptions::LIFE_BATTERY: + bAllowOniDie = true; + default: + break; + } + if( bAllowOniDie && ft == PlayerOptions::FAIL_IMMEDIATE ) + { + if( !STATSMAN->m_CurStageStats.AllFailed() ) // if not the last one to fail + { + // kill them! + SOUND->PlayOnceFromDir( THEME->GetPathS(m_sName,"oni die") ); + pi->ShowOniGameOver(); + int tracks = pi->m_NoteData.GetNumTracks(); + pi->m_NoteData.Init(); // remove all notes and scoring + pi->m_NoteData.SetNumTracks(tracks); // reset the number of tracks. + pi->m_pPlayer->FadeToFail(); // tell the NoteField to fade to white + } + } + } + + bool bAllFailed = true; + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + PlayerOptions::FailType ft = GAMESTATE->GetPlayerFailType( pi->GetPlayerState() ); + switch( ft ) + { + case PlayerOptions::FAIL_IMMEDIATE: + if( pi->m_pLifeMeter == NULL || (pi->m_pLifeMeter && !pi->m_pLifeMeter->IsFailing()) ) + bAllFailed = false; + break; + case PlayerOptions::FAIL_IMMEDIATE_CONTINUE: + case PlayerOptions::FAIL_AT_END: + bAllFailed = false; // wait until the end of the song to fail. + break; + case PlayerOptions::FAIL_OFF: + bAllFailed = false; // never fail. + break; + default: + FAIL_M("Invalid fail type! Aborting..."); + } + } + + if( bAllFailed ) + { + m_pSoundMusic->StopPlaying(); + SCREENMAN->PostMessageToTopScreen( SM_NotesEnded, 0 ); + m_LyricDisplay.Stop(); + } + + // Update living players' alive time + // HACK: Don't scale alive time when using tab/tilde. Instead of accumulating time from a timer, + // this time should instead be tied to the music position. + float fUnscaledDeltaTime = m_timerGameplaySeconds.GetDeltaTime(); + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + if( !pi->GetPlayerStageStats()->m_bFailed ) + pi->GetPlayerStageStats()->m_fAliveSeconds += fUnscaledDeltaTime * GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; + + // update fGameplaySeconds + STATSMAN->m_CurStageStats.m_fGameplaySeconds += fUnscaledDeltaTime; + float curBeat = GAMESTATE->m_Position.m_fSongBeat; + Song &s = *GAMESTATE->m_pCurSong; + + if( curBeat >= s.GetFirstBeat() && curBeat < s.GetLastBeat() ) + { + STATSMAN->m_CurStageStats.m_fStepsSeconds += fUnscaledDeltaTime; + + if( GAMESTATE->m_SongOptions.GetCurrent().m_fHaste != 0.0f ) + { + float fHasteRate = GetHasteRate(); + GAMESTATE->m_fAccumulatedHasteSeconds += (fUnscaledDeltaTime * fHasteRate) - fUnscaledDeltaTime; + } + } + + // Check for end of song + { + float fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut; + GetMusicEndTiming( fSecondsToStartFadingOutMusic, fSecondsToStartTransitioningOut ); + + bool bAllReallyFailed = STATSMAN->m_CurStageStats.AllFailed(); + if( bAllReallyFailed ) + fSecondsToStartTransitioningOut += BEGIN_FAILED_DELAY; + + if( GAMESTATE->m_Position.m_fMusicSeconds >= fSecondsToStartTransitioningOut && !m_NextSong.IsTransitioning() ) + this->PostScreenMessage( SM_NotesEnded, 0 ); + } + + // update 2d dancing characters + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + DancingCharacters *pCharacter = NULL; + if( m_pSongBackground ) + pCharacter = m_pSongBackground->GetDancingCharacters(); + if( pCharacter != NULL ) + { + TapNoteScore tns = pi->m_pPlayer->GetLastTapNoteScore(); + + ANIM_STATES_2D state = AS2D_MISS; + + switch( tns ) + { + case TNS_W4: + case TNS_W3: + state = AS2D_GOOD; + break; + case TNS_W2: + case TNS_W1: + state = AS2D_GREAT; + break; + default: + state = AS2D_MISS; + break; + } + + if( state == AS2D_GREAT && pi->GetPlayerState()->m_HealthState == HealthState_Hot ) + state = AS2D_FEVER; + + pCharacter->Change2DAnimState( pi->m_pn, state ); + } + } + + // Check for enemy death in enemy battle + static float fLastSeenEnemyHealth = 1; + if( fLastSeenEnemyHealth != GAMESTATE->m_fOpponentHealthPercent ) + { + fLastSeenEnemyHealth = GAMESTATE->m_fOpponentHealthPercent; + + if( GAMESTATE->m_fOpponentHealthPercent == 0 ) + { + // HACK: Load incorrect directory on purpose for now. + PlayAnnouncer( "gameplay battle damage level3", 0 ); + + GAMESTATE->RemoveAllActiveAttacks(); + + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + if( !GAMESTATE->IsCpuPlayer(pi->m_pn) ) + continue; + + SOUND->PlayOnceFromDir( THEME->GetPathS(m_sName,"oni die") ); + pi->ShowOniGameOver(); + pi->m_NoteData.Init(); // remove all notes and scoring + pi->m_pPlayer->FadeToFail(); // tell the NoteField to fade to white + } + } + } + + // update give up + bool bGiveUpTimerFired = !m_GiveUpTimer.IsZero() && m_GiveUpTimer.Ago() > 2.5f; + + + bool bAllHumanHaveBigMissCombo = true; + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + if (pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_FailType == PlayerOptions::FAIL_OFF || + pi->GetPlayerState()->m_HealthState < HealthState_Dead ) + { + bAllHumanHaveBigMissCombo = false; + break; + } + } + if (bAllHumanHaveBigMissCombo) // possible to get in here. + { + bAllHumanHaveBigMissCombo = FAIL_ON_MISS_COMBO.GetValue() != -1 && STATSMAN->m_CurStageStats.GetMinimumMissCombo() >= FAIL_ON_MISS_COMBO; + } + if( bGiveUpTimerFired || bAllHumanHaveBigMissCombo ) + { + STATSMAN->m_CurStageStats.m_bGaveUp = true; + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + pi->GetPlayerStageStats()->m_bFailed |= bAllHumanHaveBigMissCombo; + pi->GetPlayerStageStats()->m_bDisqualified |= bGiveUpTimerFired; // Don't disqualify if failing for miss combo. The player should still be eligable for a high score on courses. + } + + AbortGiveUp( false ); + + if( GIVING_UP_GOES_TO_PREV_SCREEN ) + { + BeginBackingOutFromGameplay(); + } + else + { + m_pSoundMusic->StopPlaying(); + this->PostScreenMessage( SM_NotesEnded, 0 ); + } + return; + } + + // Check to see if it's time to play a ScreenGameplay comment + m_fTimeSinceLastDancingComment += fDeltaTime; + + switch( GAMESTATE->m_PlayMode ) + { + case PLAY_MODE_REGULAR: + case PLAY_MODE_BATTLE: + case PLAY_MODE_RAVE: + if( GAMESTATE->OneIsHot() ) + PlayAnnouncer( "gameplay comment hot", SECONDS_BETWEEN_COMMENTS ); + else if( GAMESTATE->AllAreInDangerOrWorse() ) + PlayAnnouncer( "gameplay comment danger", SECONDS_BETWEEN_COMMENTS ); + else + PlayAnnouncer( "gameplay comment good", SECONDS_BETWEEN_COMMENTS ); + break; + case PLAY_MODE_NONSTOP: + case PLAY_MODE_ONI: + case PLAY_MODE_ENDLESS: + PlayAnnouncer( "gameplay comment oni", SECONDS_BETWEEN_COMMENTS ); + break; + default: + ASSERT(0); + } + } + default: break; + } + + PlayTicks(); + + UpdateLights(); + + SendCrossedMessages(); + + if( NSMAN->useSMserver ) + { + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + if( pi->m_pLifeMeter ) + NSMAN->m_playerLife[pi->m_pn] = int(pi->m_pLifeMeter->GetLife()*10000); + + if( m_bShowScoreboard ) + FOREACH_NSScoreBoardColumn(cn) + if( m_bShowScoreboard && NSMAN->ChangedScoreboard(cn) && GAMESTATE->GetFirstDisabledPlayer() != PLAYER_INVALID ) + m_Scoreboard[cn].SetText( NSMAN->m_Scoreboard[cn] ); + } +} + +float ScreenGameplay::GetHasteRate() +{ + if( GAMESTATE->m_Position.m_fMusicSeconds < GAMESTATE->m_fLastHasteUpdateMusicSeconds || // new song + GAMESTATE->m_Position.m_fMusicSeconds > GAMESTATE->m_fLastHasteUpdateMusicSeconds + 4 ) + { + bool bAnyPlayerHitAllNotes = false; + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( !GAMESTATE->IsHumanPlayer(pi->m_pn) ) + continue; + + PlayerState *pPS = pi->GetPlayerState(); + if( pPS->m_iTapsHitSinceLastHasteUpdate > 0 && + pPS->m_iTapsMissedSinceLastHasteUpdate == 0 ) + bAnyPlayerHitAllNotes = true; + + pPS->m_iTapsHitSinceLastHasteUpdate = 0; + pPS->m_iTapsMissedSinceLastHasteUpdate = 0; + } + + if( bAnyPlayerHitAllNotes ) + GAMESTATE->m_fHasteRate += 0.1f; + CLAMP( GAMESTATE->m_fHasteRate, -1.0f, +1.0f ); + + GAMESTATE->m_fLastHasteUpdateMusicSeconds = GAMESTATE->m_Position.m_fMusicSeconds; + } + + /* If the life meter is less than half full, push the haste rate down to let + * the player use his accumulated haste time. */ + float fMaxLife = 0; + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( !GAMESTATE->IsHumanPlayer(pi->m_pn) ) + continue; + fMaxLife = max( fMaxLife, pi->m_pLifeMeter->GetLife() ); + } + if( fMaxLife < 0.5f ) + GAMESTATE->m_fHasteRate = SCALE( fMaxLife, 0.0f, 0.5f, -1.0f, 0.0f ); + + float fSpeed = 1.0f; + if( GAMESTATE->m_fHasteRate < 0 ) + fSpeed = SCALE( GAMESTATE->m_fHasteRate, -1.0f, 0.0f, 0.5f, 1.0f ); + else if( GAMESTATE->m_fHasteRate < 0.3f ) + fSpeed = SCALE( GAMESTATE->m_fHasteRate, 0.0f, 0.3f, 1.0f, 1.2f ); + else + fSpeed = SCALE( GAMESTATE->m_fHasteRate, 0.3f, 1.0f, 1.2f, 1.5f ); + fSpeed *= GAMESTATE->m_SongOptions.GetCurrent().m_fHaste; + + if( GAMESTATE->m_fAccumulatedHasteSeconds <= 1 ) + { + /* Only allow slowing down the song while the players have accumulated + * haste. This prevents dragging on the song by keeping the life meter + * nearly empty. */ + float fClamped = max( 1.0f, fSpeed ); + fSpeed = lerp( GAMESTATE->m_fAccumulatedHasteSeconds, fClamped, fSpeed ); + } + return fSpeed; +} + +void ScreenGameplay::UpdateLights() +{ + if( !LIGHTSMAN->IsEnabled() ) + return; + if( m_CabinetLightsNoteData.GetNumTracks() == 0 ) // light data wasn't loaded + return; + + const Style* pStyle = GAMESTATE->GetCurrentStyle(); + bool bBlinkCabinetLight[NUM_CabinetLight]; + bool bBlinkGameButton[NUM_GameController][NUM_GameButton]; + ZERO( bBlinkCabinetLight ); + ZERO( bBlinkGameButton ); + bool bCrossedABeat = false; + { + const float fSongBeat = GAMESTATE->m_Position.m_fLightSongBeat; + const int iSongRow = BeatToNoteRowNotRounded( fSongBeat ); + + static int iRowLastCrossed = 0; + + float fBeatLast = roundf(NoteRowToBeat(iRowLastCrossed)); + float fBeatNow = roundf(NoteRowToBeat(iSongRow)); + + bCrossedABeat = fBeatLast != fBeatNow; + + FOREACH_CabinetLight( cl ) + { + // for each index we crossed since the last update: + FOREACH_NONEMPTY_ROW_IN_TRACK_RANGE( m_CabinetLightsNoteData, cl, r, iRowLastCrossed+1, iSongRow+1 ) + { + if( m_CabinetLightsNoteData.GetTapNote( cl, r ).type != TapNote::empty ) + bBlinkCabinetLight[cl] = true; + } + + if( m_CabinetLightsNoteData.IsHoldNoteAtRow( cl, iSongRow ) ) + bBlinkCabinetLight[cl] = true; + } + + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + const NoteData &nd = pi->m_pPlayer->GetNoteData(); + for( int t=0; tStyleInputToGameInput( t, pi->m_pn ); + bBlinkGameButton[gi.controller][gi.button] = true; + } + } + } + + iRowLastCrossed = iSongRow; + } + + // Before the first beat of the song, all cabinet lights solid on (except for menu buttons). + Song &s = *GAMESTATE->m_pCurSong; + bool bOverrideCabinetBlink = (GAMESTATE->m_Position.m_fSongBeat < s.GetFirstBeat()); + FOREACH_CabinetLight( cl ) + bBlinkCabinetLight[cl] |= bOverrideCabinetBlink; + + // Send blink data. + FOREACH_CabinetLight( cl ) + { + if( bBlinkCabinetLight[cl] ) + LIGHTSMAN->BlinkCabinetLight( cl ); + } + + FOREACH_ENUM( GameController, gc ) + { + FOREACH_ENUM( GameButton, gb ) + { + if( bBlinkGameButton[gc][gb] ) + LIGHTSMAN->BlinkGameButton( GameInput(gc,gb) ); + } + } +} + +void ScreenGameplay::SendCrossedMessages() +{ + { + static int iRowLastCrossed = 0; + + float fPositionSeconds = GAMESTATE->m_Position.m_fMusicSeconds; + float fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds ); + + int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); + iRowNow = max( 0, iRowNow ); + + for( int r=iRowLastCrossed+1; r<=iRowNow; r++ ) + { + if( GetNoteType( r ) == NOTE_TYPE_4TH ) + MESSAGEMAN->Broadcast( Message_BeatCrossed ); + } + + iRowLastCrossed = iRowNow; + } + + { + const int NUM_MESSAGES_TO_SEND = 4; + const float MESSAGE_SPACING_SECONDS = 0.4f; + + PlayerNumber pn = PLAYER_INVALID; + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + if( GAMESTATE->m_pCurSteps[ pi->m_pn ]->GetDifficulty() == Difficulty_Beginner ) + { + pn = pi->m_pn; + break; + } + } + if( pn == PLAYER_INVALID ) + return; + + const NoteData &nd = m_vPlayerInfo[pn].m_pPlayer->GetNoteData(); + + static int iRowLastCrossedAll[NUM_MESSAGES_TO_SEND] = { 0, 0, 0, 0 }; + for( int i=0; im_Position.m_fMusicSeconds + fNoteWillCrossInSeconds; + float fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds ); + + int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); + iRowNow = max( 0, iRowNow ); + int &iRowLastCrossed = iRowLastCrossedAll[i]; + + FOREACH_NONEMPTY_ROW_ALL_TRACKS_RANGE( nd, r, iRowLastCrossed+1, iRowNow+1 ) + { + int iNumTracksWithTapOrHoldHead = 0; + for( int t=0; tGetCurrentStyle(); + RString sButton = pStyle->ColToButtonName( t ); + Message msg( i == 0 ? "NoteCrossed" : "NoteWillCross" ); + msg.SetParam( "ButtonName", sButton ); + msg.SetParam( "NumMessagesFromCrossed", i ); + MESSAGEMAN->Broadcast( msg ); + } + + if( iNumTracksWithTapOrHoldHead > 0 ) + MESSAGEMAN->Broadcast( (MessageID)(Message_NoteCrossed + i) ); + if( i == 0 && iNumTracksWithTapOrHoldHead >= 2 ) + { + RString sMessageName = "NoteCrossedJump"; + MESSAGEMAN->Broadcast( sMessageName ); + } + } + + iRowLastCrossed = iRowNow; + } + } +} + +void ScreenGameplay::BeginBackingOutFromGameplay() +{ + m_DancingState = STATE_OUTRO; + AbortGiveUp( false ); + + m_pSoundMusic->StopPlaying(); + m_GameplayAssist.StopPlaying(); // Stop any queued assist ticks. + + this->ClearMessageQueue(); + + // If this is the final stage, don't allow extra stage + if( GAMESTATE->GetSmallestNumStagesLeftForAnyHumanPlayer() == 0 ) + GAMESTATE->m_bBackedOutOfFinalStage = true; + // Disallow backing out of extra stage + if( GAMESTATE->IsAnExtraStage() ) + SCREENMAN->PostMessageToTopScreen( SM_BeginFailed, 0 ); + else + m_Cancel.StartTransitioning( SM_DoPrevScreen ); +} + +void ScreenGameplay::AbortGiveUp( bool bShowText ) +{ + if( m_GiveUpTimer.IsZero() ) + return; + + m_textDebug.StopTweening(); + if( bShowText ) + m_textDebug.SetText( GIVE_UP_ABORTED_TEXT ); + // otherwise tween out the text that's there + + m_textDebug.BeginTweening( 1/2.f ); + m_textDebug.SetDiffuse( RageColor(1,1,1,0) ); + m_GiveUpTimer.SetZero(); +} + + +void ScreenGameplay::Input( const InputEventPlus &input ) +{ + //LOG->Trace( "ScreenGameplay::Input()" ); + + Message msg(""); + if( m_Codes.InputMessage(input, msg) ) + this->HandleMessage( msg ); + + if( m_bPaused ) + { + /* If we're paused, only accept GAME_BUTTON_START to unpause. */ + if( GAMESTATE->IsHumanPlayer(input.pn) && input.MenuI == GAME_BUTTON_START && input.type == IET_FIRST_PRESS ) + { + if( m_PauseController == GameController_Invalid || m_PauseController == input.GameI.controller ) + { + // IMO, it's better to have this configurable. -DaisuMaster + if( UNPAUSE_WITH_START ) this->PauseGame( false ); + } + } + return; + } + + if( m_DancingState != STATE_OUTRO && + GAMESTATE->IsHumanPlayer(input.pn) && + !m_Cancel.IsTransitioning() ) + { + /* Allow bailing out by holding any START button. + * This gives a way to "give up" when a back button isn't available. + * If this is also a style button, don't do this; pump center is start. + */ + bool bHoldingGiveUp = false; + if( GAMESTATE->GetCurrentStyle()->GameInputToColumn(input.GameI) == Column_Invalid ) + { + bHoldingGiveUp |= ( START_GIVES_UP && input.MenuI == GAME_BUTTON_START ); + bHoldingGiveUp |= ( BACK_GIVES_UP && input.MenuI == GAME_BUTTON_BACK ); + } + + if( bHoldingGiveUp ) + { + // No PREFSMAN->m_bDelayedEscape; always delayed. + if( input.type==IET_RELEASE ) + { + AbortGiveUp( true ); + } + else if( input.type==IET_FIRST_PRESS && m_GiveUpTimer.IsZero() ) + { + m_textDebug.SetText( GIVE_UP_START_TEXT ); + m_textDebug.PlayCommand( "StartOn" ); + m_GiveUpTimer.Touch(); // start the timer + } + + return; + } + + /* Only handle GAME_BUTTON_BACK as a regular BACK button if BACK_GIVES_UP is + * disabled. */ + bool bHoldingBack = false; + if( GAMESTATE->GetCurrentStyle()->GameInputToColumn(input.GameI) == Column_Invalid ) + { + bHoldingBack |= input.MenuI == GAME_BUTTON_BACK && !BACK_GIVES_UP; + } + + if( bHoldingBack ) + { + if( ((!PREFSMAN->m_bDelayedBack && input.type==IET_FIRST_PRESS) || + (input.DeviceI.device==DEVICE_KEYBOARD && input.type==IET_REPEAT) || + (input.DeviceI.device!=DEVICE_KEYBOARD && INPUTFILTER->GetSecsHeld(input.DeviceI) >= 1.0f)) ) + { + LOG->Trace("Player %i went back", input.pn+1); + BeginBackingOutFromGameplay(); + } + else if( PREFSMAN->m_bDelayedBack && input.type==IET_FIRST_PRESS ) + { + m_textDebug.SetText( GIVE_UP_BACK_TEXT ); + m_textDebug.PlayCommand( "BackOn" ); + } + else if( PREFSMAN->m_bDelayedBack && input.type==IET_RELEASE ) + { + m_textDebug.PlayCommand( "TweenOff" ); + } + + return; + } + } + + bool bRelease = input.type == IET_RELEASE; + if( !input.GameI.IsValid() ) + return; + + int iCol = GAMESTATE->GetCurrentStyle()->GameInputToColumn( input.GameI ); + + // Don't pass on any inputs to Player that aren't a press or a release. + switch( input.type ) + { + case IET_FIRST_PRESS: + case IET_RELEASE: + break; + default: + return; + } + + if( GAMESTATE->m_bMultiplayer ) + { + if( input.mp != MultiPlayer_Invalid && GAMESTATE->IsMultiPlayerEnabled(input.mp) && iCol != -1 ) + { + FOREACH( PlayerInfo, m_vPlayerInfo, pi ) + { + if( input.mp == pi->m_mp ) + pi->m_pPlayer->Step( iCol, -1, input.DeviceI.ts, false, bRelease ); + } + } + } + else + { + // handle a step or battle item activate + if( GAMESTATE->IsHumanPlayer( input.pn ) ) + { + AbortGiveUp( true ); + + if( GamePreferences::m_AutoPlay == PC_HUMAN && GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerOptions.GetCurrent().m_fPlayerAutoPlay == 0 ) + { + PlayerInfo& pi = GetPlayerInfoForInput( input ); + + ASSERT( input.GameI.IsValid() ); + + GameButtonType gbt = GAMESTATE->m_pCurGame->GetPerButtonInfo(input.GameI.button)->m_gbt; + switch( gbt ) + { + case GameButtonType_INVALID: + break; + case GameButtonType_Step: + if( iCol != -1 ) + pi.m_pPlayer->Step( iCol, -1, input.DeviceI.ts, false, bRelease ); + break; + case GameButtonType_Fret: + if( iCol != -1 ) + pi.m_pPlayer->Fret( iCol, -1, input.DeviceI.ts, false, bRelease ); + break; + case GameButtonType_Strum: + pi.m_pPlayer->Strum( iCol, -1, input.DeviceI.ts, false, bRelease ); + break; + } + } + } + } +} + + +/* Saving StageStats that are affected by the note pattern is a little tricky: + * + * Stats are cumulative for course play. + * + * For regular songs, it doesn't matter how we do it; the pattern doesn't change + * during play. + * + * The pattern changes during play in battle and course mode. We want to include + * these changes, so run stats for a song after the song finishes. + * + * If we fail, be sure to include the current song in stats, + * with the current modifier set. So: + * 1. At the end of a song in any mode, pass or fail, add stats for that song + * (from m_pPlayer). + * 2. At the end of gameplay in course mode, add stats for any songs that weren't + * played, applying the modifiers the song would have been played with. + * This doesn't include songs that were played but failed; that was done in #1. + */ +void ScreenGameplay::SaveStats() +{ + float fMusicLen = GAMESTATE->m_pCurSong->m_fMusicLengthSeconds; + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + /* Note that adding stats is only meaningful for the counters (eg. RadarCategory_Jumps), + * not for the percentages (RadarCategory_Air). */ + RadarValues rv; + PlayerStageStats &pss = *pi->GetPlayerStageStats(); + const NoteData &nd = pi->m_pPlayer->GetNoteData(); + PlayerNumber pn = pi->m_pn; + + GAMESTATE->SetProcessedTimingData(&GAMESTATE->m_pCurSteps[pn]->m_Timing); + NoteDataUtil::CalculateRadarValues( nd, fMusicLen, rv ); + pss.m_radarPossible += rv; + NoteDataWithScoring::GetActualRadarValues( nd, pss, fMusicLen, rv ); + pss.m_radarActual += rv; + GAMESTATE->SetProcessedTimingData(NULL); + } +} + +void ScreenGameplay::SongFinished() +{ + AdjustSync::HandleSongEnd(); + SaveStats(); // Let subclasses save the stats. + /* Extremely important: if we don't remove attacks before moving on to the next + * screen, they'll still be turned on eventually. */ + GAMESTATE->RemoveAllActiveAttacks(); + FOREACH_VisiblePlayerInfo( m_vPlayerInfo, pi ) + pi->m_pActiveAttackList->Refresh(); +} + +void ScreenGameplay::StageFinished( bool bBackedOut ) +{ + if( GAMESTATE->IsCourseMode() && GAMESTATE->m_PlayMode != PLAY_MODE_ENDLESS ) + { + LOG->Trace("Stage finished at index %i/%i", GAMESTATE->GetCourseSongIndex(), (int) m_apSongsQueue.size() ); + // +1 to skip the current song; that song has already passed. + for( unsigned i = GAMESTATE->GetCourseSongIndex()+1; i < m_apSongsQueue.size(); ++i ) + { + LOG->Trace("Running stats for %i", i ); + SetupSong( i ); + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + pi->m_pPlayer->ApplyWaitingTransforms(); + SongFinished(); + } + } + + if( bBackedOut ) + { + GAMESTATE->CancelStage(); + return; + } + + // If all players failed, kill. + if( STATSMAN->m_CurStageStats.AllFailed() ) + { + FOREACH_HumanPlayer( p ) + GAMESTATE->m_iPlayerStageTokens[p] = 0; + } + + FOREACH_HumanPlayer( pn ) + STATSMAN->m_CurStageStats.m_player[pn].CalcAwards( pn, STATSMAN->m_CurStageStats.m_bGaveUp, STATSMAN->m_CurStageStats.m_bUsedAutoplay ); + STATSMAN->m_CurStageStats.FinalizeScores( false ); + + GAMESTATE->CommitStageStats(); + + // save current stage stats + STATSMAN->m_vPlayedStageStats.push_back( STATSMAN->m_CurStageStats ); + + STATSMAN->CalcAccumPlayedStageStats(); + GAMESTATE->FinishStage(); +} + +void ScreenGameplay::HandleScreenMessage( const ScreenMessage SM ) +{ + CHECKPOINT_M( ssprintf("HandleScreenMessage(%s)", ScreenMessageHelpers::ScreenMessageToString(SM).c_str()) ); + if( SM == SM_DoneFadingIn ) + { + SOUND->PlayOnceFromAnnouncer( "gameplay ready" ); + m_Ready.StartTransitioning( SM_PlayGo ); + } + else if( SM == SM_PlayGo ) + { + if( GAMESTATE->IsAnExtraStage() ) + SOUND->PlayOnceFromAnnouncer( "gameplay here we go extra" ); + else if( GAMESTATE->GetSmallestNumStagesLeftForAnyHumanPlayer() == 0 ) + SOUND->PlayOnceFromAnnouncer( "gameplay here we go final" ); + else + SOUND->PlayOnceFromAnnouncer( "gameplay here we go normal" ); + + m_Go.StartTransitioning( SM_None ); + GAMESTATE->m_bGameplayLeadIn.Set( false ); + m_DancingState = STATE_DANCING; // STATE CHANGE! Now the user is allowed to press Back + } + else if( SM == SM_NotesEnded ) // received while STATE_DANCING + { + AbortGiveUp( false ); // don't allow giveup while the next song is loading + + /* Do this in LoadNextSong, so we don't tween off old attacks until + * m_NextSong finishes. */ + // GAMESTATE->RemoveAllActiveAttacks(); + + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + // Mark failure. + if( GAMESTATE->GetPlayerFailType(pi->GetPlayerState()) != PlayerOptions::FAIL_OFF && + (pi->m_pLifeMeter && pi->m_pLifeMeter->IsFailing()) ) + pi->GetPlayerStageStats()->m_bFailed = true; + + if( !pi->GetPlayerStageStats()->m_bFailed ) + pi->GetPlayerStageStats()->m_iSongsPassed++; + + // set a life record at the point of failure + if( pi->GetPlayerStageStats()->m_bFailed ) + pi->GetPlayerStageStats()->SetLifeRecordAt( 0, STATSMAN->m_CurStageStats.m_fGameplaySeconds ); + } + + /* If all players have *really* failed (bFailed, not the life meter or + * bFailedEarlier): */ + const bool bAllReallyFailed = STATSMAN->m_CurStageStats.AllFailed(); + const bool bStopCourseEarly = STOP_COURSE_EARLY; + const bool bIsLastSong = IsLastSong(); + + LOG->Trace( "bAllReallyFailed = %d, bStopCourseEarly = %d, bIsLastSong = %d", bAllReallyFailed, bStopCourseEarly, bIsLastSong ); + + if( bStopCourseEarly || bAllReallyFailed || bIsLastSong ) + { + // Time to leave from ScreenGameplay + HandleScreenMessage( SM_LeaveGameplay ); + } + else + { + // Load the next song in the course. + HandleScreenMessage( SM_StartLoadingNextSong ); + return; + } + } + else if( SM == SM_LeaveGameplay ) + { + // update dancing characters for win / lose + DancingCharacters *pDancers = NULL; + if( m_pSongBackground ) + pDancers = m_pSongBackground->GetDancingCharacters(); + if( pDancers ) + { + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + // XXX: In battle modes, switch( GAMESTATE->GetStageResult(p) ). + if( pi->GetPlayerStageStats()->m_bFailed ) + pDancers->Change2DAnimState( pi->m_pn, AS2D_FAIL ); // fail anim + else if( pi->m_pLifeMeter && pi->GetPlayerState()->m_HealthState == HealthState_Hot ) + pDancers->Change2DAnimState( pi->m_pn, AS2D_WINFEVER ); // full life pass anim + else + pDancers->Change2DAnimState( pi->m_pn, AS2D_WIN ); // pass anim + } + } + + // End round. + if( m_DancingState == STATE_OUTRO ) // ScreenGameplay already ended + return; // ignore + m_DancingState = STATE_OUTRO; + AbortGiveUp( false ); + + GAMESTATE->RemoveAllActiveAttacks(); + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( pi->m_pActiveAttackList ) + pi->m_pActiveAttackList->Refresh(); + } + + LIGHTSMAN->SetLightsMode( LIGHTSMODE_ALL_CLEARED ); + + bool bAllReallyFailed = STATSMAN->m_CurStageStats.AllFailed(); + + if( bAllReallyFailed ) + { + this->PostScreenMessage( SM_BeginFailed, 0 ); + return; + } + + // todo: add GameplayCleared, StartTransitioningCleared commands -aj + TweenOffScreen(); + + m_Out.StartTransitioning( SM_DoNextScreen ); + + // do they deserve an extra stage? + if( GAMESTATE->HasEarnedExtraStage() ) + SOUND->PlayOnceFromAnnouncer( "gameplay extra" ); + else + SOUND->PlayOnceFromAnnouncer( "gameplay cleared" ); + } + else if( SM == SM_StartLoadingNextSong ) + { + // Next song. + // give a little life back between stages + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( pi->m_pLifeMeter ) + pi->m_pLifeMeter->OnSongEnded(); + } + + GAMESTATE->m_bLoadingNextSong = true; + MESSAGEMAN->Broadcast( "BeforeLoadingNextCourseSong" ); + m_NextSong.Reset(); + m_NextSong.PlayCommand( "Start" ); + m_NextSong.StartTransitioning( SM_LoadNextSong ); + MESSAGEMAN->Broadcast( "ChangeCourseSongIn" ); + } + else if( SM == SM_LoadNextSong ) + { + m_pSoundMusic->Stop(); + SongFinished(); + + MESSAGEMAN->Broadcast( "ChangeCourseSongOut" ); + + GAMESTATE->m_bLoadingNextSong = false; + LoadNextSong(); + + m_NextSong.Reset(); + m_NextSong.PlayCommand( "Finish" ); + m_NextSong.StartTransitioning( SM_None ); + + StartPlayingSong( MIN_SECONDS_TO_STEP_NEXT_SONG, 0 ); + } + else if( SM == SM_PlayToasty ) + { + // todo: make multiple toasties work -aj + if( g_bEasterEggs ) + if( !m_Toasty.IsTransitioning() && !m_Toasty.IsFinished() ) // don't play if we've already played it once + m_Toasty.StartTransitioning(); + } + else if( ScreenMessageHelpers::ScreenMessageToString(SM).find("0Combo") != string::npos ) + { + int iCombo; + RString sCropped = ScreenMessageHelpers::ScreenMessageToString(SM).substr(3); + sscanf(sCropped.c_str(),"%d%*s",&iCombo); + PlayAnnouncer( ssprintf("gameplay %d combo",iCombo), 2 ); + } + else if( SM == SM_ComboStopped ) + { + PlayAnnouncer( "gameplay combo stopped", 2 ); + } + else if( SM == SM_ComboContinuing ) + { + PlayAnnouncer( "gameplay combo overflow", 2 ); + } + else if( SM >= SM_BattleTrickLevel1 && SM <= SM_BattleTrickLevel3 ) + { + int iTrickLevel = SM-SM_BattleTrickLevel1+1; + PlayAnnouncer( ssprintf("gameplay battle trick level%d",iTrickLevel), 3 ); + if( SM == SM_BattleTrickLevel1 ) m_soundBattleTrickLevel1.Play(); + else if( SM == SM_BattleTrickLevel2 ) m_soundBattleTrickLevel2.Play(); + else if( SM == SM_BattleTrickLevel3 ) m_soundBattleTrickLevel3.Play(); + } + else if( SM >= SM_BattleDamageLevel1 && SM <= SM_BattleDamageLevel3 ) + { + int iDamageLevel = SM-SM_BattleDamageLevel1+1; + PlayAnnouncer( ssprintf("gameplay battle damage level%d",iDamageLevel), 3 ); + } + else if( SM == SM_DoPrevScreen ) + { + SongFinished(); + this->StageFinished( true ); + + m_sNextScreen = GetPrevScreen(); + + if( AdjustSync::IsSyncDataChanged() ) + ScreenSaveSync::PromptSaveSync( SM_GoToPrevScreen ); + else + HandleScreenMessage( SM_GoToPrevScreen ); + } + else if( SM == SM_DoNextScreen ) + { + SongFinished(); + this->StageFinished( false ); + // only save replays if the player chose to + if( GAMESTATE->m_SongOptions.GetCurrent().m_bSaveReplay ) + SaveReplay(); + + if( AdjustSync::IsSyncDataChanged() ) + ScreenSaveSync::PromptSaveSync( SM_GoToNextScreen ); + else + HandleScreenMessage( SM_GoToNextScreen ); + } + else if( SM == SM_GainFocus ) + { + // We do this ourself. + SOUND->HandleSongTimer( false ); + } + else if( SM == SM_LoseFocus ) + { + // We might have turned the song timer off. Be sure to turn it back on. + SOUND->HandleSongTimer( true ); + } + else if( SM == SM_BeginFailed ) + { + m_DancingState = STATE_OUTRO; + AbortGiveUp( false ); + m_GameplayAssist.StopPlaying(); // Stop any queued assist ticks. + TweenOffScreen(); + m_Failed.StartTransitioning( SM_DoNextScreen ); + + // show the survive time if extra stage + if( GAMESTATE->IsAnExtraStage() ) + { + float fMaxAliveSeconds = 0; + FOREACH_EnabledPlayer(p) + fMaxAliveSeconds = max( fMaxAliveSeconds, STATSMAN->m_CurStageStats.m_player[p].m_fAliveSeconds ); + m_textSurviveTime.SetText( "TIME: " + SecondsToMMSSMsMs(fMaxAliveSeconds) ); + ON_COMMAND( m_textSurviveTime ); + } + + if( GAMESTATE->IsCourseMode() ) + { + if( GAMESTATE->GetCourseSongIndex() >= int(m_apSongsQueue.size() / 2) ) + SOUND->PlayOnceFromAnnouncer( "gameplay oni failed halfway" ); + else + SOUND->PlayOnceFromAnnouncer( "gameplay oni failed" ); + } + else + { + SOUND->PlayOnceFromAnnouncer( "gameplay failed" ); + } + } + else if( SM == SM_Pause ) + { + // Ignore SM_Pause when in demonstration. + if( GAMESTATE->m_bDemonstrationOrJukebox ) + return; + + if( !m_bPaused ) + PauseGame( true ); + } + + ScreenWithMenuElements::HandleScreenMessage( SM ); +} + +void ScreenGameplay::HandleMessage( const Message &msg ) +{ + if( msg == "Judgment" ) + { + PlayerNumber pn; + msg.GetParam( "Player", pn ); + + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + if( pi->m_pn != pn ) + continue; + if( !pi->GetPlayerState()->m_PlayerOptions.GetCurrent().m_bMuteOnError ) + continue; + + RageSoundReader *pSoundReader = m_AutoKeysounds.GetPlayerSound( pn ); + if( pSoundReader == NULL ) + pSoundReader = m_AutoKeysounds.GetSharedSound(); + + HoldNoteScore hns; + msg.GetParam( "HoldNoteScore", hns ); + TapNoteScore tns; + msg.GetParam( "TapNoteScore", tns ); + + bool bOn = false; + if( hns != HoldNoteScore_Invalid ) + bOn = hns != HNS_LetGo; + else + bOn = tns != TNS_Miss; + + if( pSoundReader ) + pSoundReader->SetProperty( "Volume", bOn? 1.0f:0.0f ); + } + } + + ScreenWithMenuElements::HandleMessage( msg ); +} + +void ScreenGameplay::Cancel( ScreenMessage smSendWhenDone ) +{ + m_pSoundMusic->Stop(); + + ScreenWithMenuElements::Cancel( smSendWhenDone ); +} + +Song *ScreenGameplay::GetNextCourseSong() const +{ + ASSERT( GAMESTATE->IsCourseMode() ); + + int iPlaySongIndex = GAMESTATE->GetCourseSongIndex()+1; + iPlaySongIndex %= m_apSongsQueue.size(); + + return m_apSongsQueue[iPlaySongIndex]; +} + +PlayerInfo *ScreenGameplay::GetPlayerInfo( PlayerNumber pn ) +{ + FOREACH_EnabledPlayerNumberInfo( m_vPlayerInfo, pi ) + { + if( pi->m_pn == pn ) + return &*pi; + } + return NULL; +} + +PlayerInfo *ScreenGameplay::GetDummyPlayerInfo( int iDummyIndex ) +{ + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + if( pi->m_bIsDummy && pi->m_iDummyIndex == iDummyIndex ) + return &*pi; + } + return NULL; +} + +void ScreenGameplay::SaveReplay() +{ + /* Replay data TODO: + * Add more player information (?) + * Add AutoGen flag if steps were autogen? + * Add proper steps hash? + * Add modifiers used + * Add date played, machine played on, etc. + * Hash of some stuff to validate data (see Profile) + */ + FOREACH_HumanPlayer( pn ) + { + FOREACH_EnabledPlayerInfo( m_vPlayerInfo, pi ) + { + Profile *pTempProfile = PROFILEMAN->GetProfile(pn); + + XNode *p = new XNode("ReplayData"); + // append version number (in case the format changes) + p->AppendAttr("Version",0); + + // song information node + SongID songID; + songID.FromSong(GAMESTATE->m_pCurSong); + XNode *pSongInfoNode = songID.CreateNode(); + pSongInfoNode->AppendChild("Title", GAMESTATE->m_pCurSong->GetDisplayFullTitle()); + pSongInfoNode->AppendChild("Artist", GAMESTATE->m_pCurSong->GetDisplayArtist()); + p->AppendChild(pSongInfoNode); + + // steps information + StepsID stepsID; + stepsID.FromSteps( GAMESTATE->m_pCurSteps[pn] ); + XNode *pStepsInfoNode = stepsID.CreateNode(); + // hashing = argh + //pStepsInfoNode->AppendChild("StepsHash", stepsID.ToSteps(GAMESTATE->m_pCurSong,false)->GetHash()); + p->AppendChild(pStepsInfoNode); + + // player information node (rival data sup) + XNode *pPlayerInfoNode = new XNode("Player"); + pPlayerInfoNode->AppendChild("DisplayName", pTempProfile->m_sDisplayName); + pPlayerInfoNode->AppendChild("Guid", pTempProfile->m_sGuid); + p->AppendChild(pPlayerInfoNode); + + // the timings. + p->AppendChild( pi->m_pPlayer->GetNoteData().CreateNode() ); + + // Find a file name for the replay + vector files; + GetDirListing( "Save/Replays/replay*", files, false, false ); + sort( files.begin(), files.end() ); + + // Files should be of the form "replay#####.xml". + int iIndex = 0; + + for( int i = files.size()-1; i >= 0; --i ) + { + static Regex re( "^replay([0-9]{5})\\....$" ); + vector matches; + if( !re.Compare( files[i], matches ) ) + continue; + + ASSERT( matches.size() == 1 ); + iIndex = StringToInt( matches[0] )+1; + break; + } + + RString sFileName = ssprintf( "replay%05d.xml", iIndex ); + + XmlFileUtil::SaveToFile( p, "Save/Replays/"+sFileName ); + SAFE_DELETE( p ); + return; + } + } +} + +/* +bool ScreenGameplay::LoadReplay() +{ + // Load replay which was selected via options +} +*/ + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the ScreenGameplay. */ +class LunaScreenGameplay: public Luna +{ +public: + static int GetNextCourseSong( T* p, lua_State *L ) { p->GetNextCourseSong()->PushSelf(L); return 1; } + static int Center1Player( T* p, lua_State *L ) { lua_pushboolean( L, p->Center1Player() ); return 1; } + static int GetLifeMeter( T* p, lua_State *L ) + { + PlayerNumber pn = Enum::Check( L, 1 ); + + PlayerInfo *pi = p->GetPlayerInfo(pn); + if( pi == NULL ) + return 0; + LifeMeter *pLM = pi->m_pLifeMeter; + if( pLM == NULL ) + return 0; + + pLM->PushSelf( L ); + return 1; + } + static int GetPlayerInfo( T* p, lua_State *L ) + { + PlayerNumber pn = Enum::Check( L, 1 ); + + PlayerInfo *pi = p->GetPlayerInfo(pn); + if( pi == NULL ) + return 0; + + pi->PushSelf( L ); + return 1; + } + static int GetDummyPlayerInfo( T* p, lua_State *L ) + { + int iDummyIndex = IArg(1); + PlayerInfo *pi = p->GetDummyPlayerInfo(iDummyIndex); + if( pi == NULL ) + return 0; + pi->PushSelf( L ); + return 1; + } + static int PauseGame( T* p, lua_State *L ) { p->Pause( BArg(1)); return 0; } + static int IsPaused( T* p, lua_State *L ) { lua_pushboolean( L, p->IsPaused() ); return 1; } + + LunaScreenGameplay() + { + ADD_METHOD( GetNextCourseSong ); + ADD_METHOD( Center1Player ); + ADD_METHOD( GetLifeMeter ); + ADD_METHOD( GetPlayerInfo ); + ADD_METHOD( GetDummyPlayerInfo ); + // sm-ssc additions: + ADD_METHOD( PauseGame ); + ADD_METHOD( IsPaused ); + } +}; + +LUA_REGISTER_DERIVED_CLASS( ScreenGameplay, ScreenWithMenuElements ) + + +/** @brief Allow Lua to have access to the PlayerInfo. */ +class LunaPlayerInfo: public Luna +{ +public: + static int GetLifeMeter( T* p, lua_State *L ) { p->m_pLifeMeter->PushSelf(L); return 1; } + static int GetStepsQueueWrapped( T* p, lua_State *L ) + { + int iIndex = IArg(1); + iIndex %= p->m_vpStepsQueue.size(); + Steps *pSteps = p->m_vpStepsQueue[iIndex]; + pSteps->PushSelf(L); + return 1; + } + + LunaPlayerInfo() + { + ADD_METHOD( GetLifeMeter ); + ADD_METHOD( GetStepsQueueWrapped ); + } +}; + +LUA_REGISTER_CLASS( PlayerInfo ) +// lua end + +/* + * (c) 2001-2004 Chris Danford, Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/ScreenHowToPlay.cpp b/src/ScreenHowToPlay.cpp index 8f97cac451..16d5ae3f62 100644 --- a/src/ScreenHowToPlay.cpp +++ b/src/ScreenHowToPlay.cpp @@ -1,353 +1,356 @@ -#include "global.h" -#include "ScreenHowToPlay.h" -#include "ThemeManager.h" -#include "GameState.h" -#include "Steps.h" -#include "GameManager.h" -#include "NotesLoaderSM.h" -#include "NotesLoaderSSC.h" -#include "GameSoundManager.h" -#include "Model.h" -#include "ThemeMetric.h" -#include "PlayerState.h" -#include "Style.h" -#include "PrefsManager.h" -#include "CharacterManager.h" -#include "StatsManager.h" -#include "RageDisplay.h" -#include "SongUtil.h" -#include "Character.h" -#include "LifeMeterBar.h" - -static const ThemeMetric NUM_W2S ("ScreenHowToPlay","NumW2s"); -static const ThemeMetric NUM_MISSES ("ScreenHowToPlay","NumMisses"); -static const ThemeMetric USE_CHARACTER ("ScreenHowToPlay","UseCharacter"); -static const ThemeMetric USE_PAD ("ScreenHowToPlay","UsePad"); -static const ThemeMetric USE_PLAYER ("ScreenHowToPlay","UsePlayer"); -static const ThemeMetric CHARACTER_NAME("ScreenHowToPlay","CharacterName"); - -enum Animation -{ - ANIM_DANCE_PAD, - ANIM_DANCE_PADS, - ANIM_UP, - ANIM_DOWN, - ANIM_LEFT, - ANIM_RIGHT, - ANIM_JUMPLR, - NUM_ANIMATIONS -}; - -static const RString anims[NUM_ANIMATIONS] = -{ - "DancePad.txt", - "DancePads.txt", - "BeginnerHelper_step-up.bones.txt", - "BeginnerHelper_step-down.bones.txt", - "BeginnerHelper_step-left.bones.txt", - "BeginnerHelper_step-right.bones.txt", - "BeginnerHelper_step-jumplr.bones.txt" -}; - -static RString GetAnimPath( Animation a ) -{ - return RString("Characters/") + anims[a]; -} - -static bool HaveAllCharAnimations() -{ - for( int i = ANIM_UP; i < NUM_ANIMATIONS; ++i ) - if( !DoesFileExist( GetAnimPath( (Animation) i ) ) ) - return false; - return true; -} - -REGISTER_SCREEN_CLASS( ScreenHowToPlay ); -ScreenHowToPlay::ScreenHowToPlay() -{ - m_iW2s = 0; - m_iNumW2s = NUM_W2S; - - // initialize these because they might not be used. - m_pLifeMeterBar = NULL; - m_pmCharacter = NULL; - m_pmDancePad = NULL; -} - -void ScreenHowToPlay::Init() -{ - ScreenAttract::Init(); - - if( (bool)USE_PAD && DoesFileExist( GetAnimPath(ANIM_DANCE_PAD) ) ) - { - m_pmDancePad = new Model; - m_pmDancePad->SetName( "Pad" ); - m_pmDancePad->LoadMilkshapeAscii( GetAnimPath(ANIM_DANCE_PAD) ); - // xxx: hardcoded rotation. can be undone, but still. -freem - m_pmDancePad->SetRotationX( 35 ); - ActorUtil::LoadAllCommandsAndSetXY( m_pmDancePad, m_sName ); - } - - // Display a character - vector vpCharacters; - CHARMAN->GetCharacters( vpCharacters ); - if( (bool)USE_CHARACTER && vpCharacters.size() && HaveAllCharAnimations() ) - { - Character* displayChar; - if( !CHARACTER_NAME.GetValue().empty() && CHARMAN->GetCharacterFromID(CHARACTER_NAME) ) - displayChar = CHARMAN->GetCharacterFromID(CHARACTER_NAME); - else - displayChar = CHARMAN->GetRandomCharacter(); - - RString sModelPath = displayChar->GetModelPath(); - if( sModelPath != "" ) - { - m_pmCharacter = new Model; - m_pmCharacter->SetName( "Character" ); - m_pmCharacter->LoadMilkshapeAscii( displayChar->GetModelPath() ); - m_pmCharacter->LoadMilkshapeAsciiBones( "Step-LEFT", GetAnimPath( ANIM_LEFT ) ); - m_pmCharacter->LoadMilkshapeAsciiBones( "Step-DOWN", GetAnimPath( ANIM_DOWN ) ); - m_pmCharacter->LoadMilkshapeAsciiBones( "Step-UP", GetAnimPath( ANIM_UP ) ); - m_pmCharacter->LoadMilkshapeAsciiBones( "Step-RIGHT", GetAnimPath( ANIM_RIGHT ) ); - m_pmCharacter->LoadMilkshapeAsciiBones( "Step-JUMPLR", GetAnimPath( ANIM_JUMPLR ) ); - RString sRestFile = displayChar->GetRestAnimationPath(); - ASSERT( !sRestFile.empty() ); - m_pmCharacter->LoadMilkshapeAsciiBones( "rest",displayChar->GetRestAnimationPath() ); - m_pmCharacter->SetDefaultAnimation( "rest" ); - m_pmCharacter->PlayAnimation( "rest" ); // Stay bouncing after a step has finished animating. - - // xxx: hardcoded rotation. can be undone, but still. -freem - m_pmCharacter->SetRotationX( 40 ); - m_pmCharacter->SetCullMode( CULL_NONE ); // many of the models floating around have the vertex order flipped - ActorUtil::LoadAllCommandsAndSetXY( m_pmCharacter, m_sName ); - } - } - - GAMESTATE->SetCurrentStyle( GAMEMAN->GetHowToPlayStyleForGame(GAMESTATE->m_pCurGame) ); - - if( USE_PLAYER ) - { - m_pLifeMeterBar = new LifeMeterBar; - m_pLifeMeterBar->SetName("LifeMeterBar"); - m_pLifeMeterBar->Load( GAMESTATE->m_pPlayerState[PLAYER_1], &STATSMAN->m_CurStageStats.m_player[PLAYER_1] ); - ActorUtil::LoadAllCommandsAndSetXY( m_pLifeMeterBar, m_sName ); - m_pLifeMeterBar->FillForHowToPlay( NUM_W2S, NUM_MISSES ); - - // Allow themers to use either a .ssc or .sm file for this. -aj - RString sStepsPath = THEME->GetPathO(m_sName, "steps"); - SSCLoader loaderSSC; - SMLoader loaderSM; - if( sStepsPath.Right(4) == ".ssc" ) - loaderSSC.LoadFromSimfile( sStepsPath, m_Song, false ); - else - loaderSM.LoadFromSimfile( sStepsPath, m_Song, false ); - m_Song.AddAutoGenNotes(); - - const Style* pStyle = GAMESTATE->GetCurrentStyle(); - - Steps *pSteps = SongUtil::GetClosestNotes( &m_Song, pStyle->m_StepsType, Difficulty_Beginner ); - ASSERT_M( pSteps != NULL, ssprintf("No playable steps of StepsType '%s' for ScreenHowToPlay", StringConversion::ToString(pStyle->m_StepsType).c_str()) ); - - pSteps->m_Timing.TidyUpData(); - NoteData tempNoteData; - pSteps->GetNoteData( tempNoteData ); - pStyle->GetTransformedNoteDataForStyle( PLAYER_1, tempNoteData, m_NoteData ); - - GAMESTATE->m_pCurSong.Set( &m_Song ); - GAMESTATE->m_pCurSteps[PLAYER_1].Set(pSteps); - GAMESTATE->m_bGameplayLeadIn.Set( false ); - GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerController = PC_AUTOPLAY; - - m_Player->Init( - "Player", - GAMESTATE->m_pPlayerState[PLAYER_1], - NULL, - m_pLifeMeterBar, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL ); - m_Player.Load( m_NoteData ); - m_Player->SetName( "Player" ); - this->AddChild( m_Player ); - ActorUtil::LoadAllCommandsAndSetXY( m_Player, m_sName ); - - // Don't show judgment - PO_GROUP_ASSIGN( GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerOptions, ModsLevel_Stage, m_fBlind, 1.0f ); - GAMESTATE->SetMasterPlayerNumber(PLAYER_1); - GAMESTATE->m_bDemonstrationOrJukebox = true; - } - - // deferred until after the player, so the notes go under it - if( m_pLifeMeterBar ) - this->AddChild( m_pLifeMeterBar ); - if( m_pmDancePad ) - this->AddChild( m_pmDancePad ); - if( m_pmCharacter ) - this->AddChild( m_pmCharacter ); - - m_fFakeSecondsIntoSong = 0; - - this->MoveToTail( &m_In ); - this->MoveToTail( &m_Out ); -} - -ScreenHowToPlay::~ScreenHowToPlay() -{ - delete m_pLifeMeterBar; - delete m_pmCharacter; - delete m_pmDancePad; -} - -void ScreenHowToPlay::Step() -{ -// xxx: assumes dance. -freem -#define ST_LEFT 0x01 -#define ST_DOWN 0x02 -#define ST_UP 0x04 -#define ST_RIGHT 0x08 -#define ST_JUMPLR (ST_LEFT | ST_RIGHT) -#define ST_JUMPUD (ST_UP | ST_DOWN) - - int iStep = 0; - const int iNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat + 0.6f ); - // if we want to miss from here on out, don't process steps. - if( m_iW2s < m_iNumW2s && m_NoteData.IsThereATapAtRow( iNoteRow ) ) - { - const int iNumTracks = m_NoteData.GetNumTracks(); - for( int k=0; kPlayAnimation( "Step-LEFT", 1.8f ); break; - case ST_RIGHT: m_pmCharacter->PlayAnimation( "Step-RIGHT", 1.8f ); break; - case ST_UP: m_pmCharacter->PlayAnimation( "Step-UP", 1.8f ); break; - case ST_DOWN: m_pmCharacter->PlayAnimation( "Step-DOWN", 1.8f ); break; - case ST_JUMPLR: m_pmCharacter->PlayAnimation( "Step-JUMPLR", 1.8f ); break; - case ST_JUMPUD: - // Until I can get an UP+DOWN jump animation, this will have to do. - m_pmCharacter->PlayAnimation( "Step-JUMPLR", 1.8f ); - - m_pmCharacter->StopTweening(); - m_pmCharacter->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /8, TWEEN_LINEAR ); - m_pmCharacter->SetRotationY( 90 ); - m_pmCharacter->BeginTweening( (1/(GAMESTATE->m_Position.m_fCurBPS * 2) ) ); //sleep between jump-frames - m_pmCharacter->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /6, TWEEN_LINEAR ); - m_pmCharacter->SetRotationY( 0 ); - break; - } - } -} - -void ScreenHowToPlay::Update( float fDelta ) -{ - if( GAMESTATE->m_pCurSong != NULL ) - { - GAMESTATE->UpdateSongPosition( m_fFakeSecondsIntoSong, GAMESTATE->m_pCurSong->m_SongTiming ); - m_fFakeSecondsIntoSong += fDelta; - - static int iLastNoteRowCounted = 0; - int iCurNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat ); - - if( iCurNoteRow != iLastNoteRowCounted &&m_NoteData.IsThereATapAtRow(iCurNoteRow) ) - { - if( m_pLifeMeterBar && !m_Player ) - { - if ( m_iW2s < m_iNumW2s ) - m_pLifeMeterBar->ChangeLife( TNS_W2 ); - else - m_pLifeMeterBar->ChangeLife( TNS_Miss ); - } - m_iW2s++; - iLastNoteRowCounted = iCurNoteRow; - } - - // Once we hit the number of perfects we want, we want to fail. Switch - // the controller to HUMAN. Since we aren't taking input, the steps will - // always be misses. - if( m_iW2s > m_iNumW2s ) - GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerController = PC_HUMAN; - - // Per the above code, we don't always want the character stepping. - // If they try to make all of the steps in the miss part, they look - // silly. Have then stand still instead. - freem - if ( m_pmCharacter ) - { - if( m_iW2s <= m_iNumW2s ) - Step(); - } - } - - ScreenAttract::Update( fDelta ); -} - -void ScreenHowToPlay::HandleScreenMessage( const ScreenMessage SM ) -{ - if( SM == SM_GainFocus ) - { - // We do this ourself. - SOUND->HandleSongTimer( false ); - } - else if( SM == SM_LoseFocus ) - { - SOUND->HandleSongTimer( true ); - } - else if( SM == SM_GoToNextScreen ) - { - GAMESTATE->Reset(); - } - ScreenAttract::HandleScreenMessage( SM ); -} - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the ScreenHowToPlay. */ -class LunaScreenHowToPlay: public Luna -{ -public: - static int GetLifeMeter( T* p, lua_State *L ) - { - //PlayerNumber pn = Enum::Check( L, 1 ); - - p->m_pLifeMeterBar->PushSelf( L ); - return 1; - } - - LunaScreenHowToPlay() - { - ADD_METHOD( GetLifeMeter ); - } -}; - -LUA_REGISTER_DERIVED_CLASS( ScreenHowToPlay, ScreenAttract ) -// lua end - -/* - * (c) 2001-2004 Chris Danford, Thad Ward - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#include "global.h" +#include "ScreenHowToPlay.h" +#include "ThemeManager.h" +#include "GameState.h" +#include "Steps.h" +#include "GameManager.h" +#include "NotesLoaderSM.h" +#include "NotesLoaderSSC.h" +#include "GameSoundManager.h" +#include "Model.h" +#include "ThemeMetric.h" +#include "PlayerState.h" +#include "Style.h" +#include "PrefsManager.h" +#include "CharacterManager.h" +#include "StatsManager.h" +#include "RageDisplay.h" +#include "SongUtil.h" +#include "Character.h" +#include "LifeMeterBar.h" + +static const ThemeMetric NUM_W2S ("ScreenHowToPlay","NumW2s"); +static const ThemeMetric NUM_MISSES ("ScreenHowToPlay","NumMisses"); +static const ThemeMetric USE_CHARACTER ("ScreenHowToPlay","UseCharacter"); +static const ThemeMetric USE_PAD ("ScreenHowToPlay","UsePad"); +static const ThemeMetric USE_PLAYER ("ScreenHowToPlay","UsePlayer"); +static const ThemeMetric CHARACTER_NAME("ScreenHowToPlay","CharacterName"); + +enum Animation +{ + ANIM_DANCE_PAD, + ANIM_DANCE_PADS, + ANIM_UP, + ANIM_DOWN, + ANIM_LEFT, + ANIM_RIGHT, + ANIM_JUMPLR, + NUM_ANIMATIONS +}; + +static const RString anims[NUM_ANIMATIONS] = +{ + "DancePad.txt", + "DancePads.txt", + "BeginnerHelper_step-up.bones.txt", + "BeginnerHelper_step-down.bones.txt", + "BeginnerHelper_step-left.bones.txt", + "BeginnerHelper_step-right.bones.txt", + "BeginnerHelper_step-jumplr.bones.txt" +}; + +static RString GetAnimPath( Animation a ) +{ + return RString("Characters/") + anims[a]; +} + +static bool HaveAllCharAnimations() +{ + for( int i = ANIM_UP; i < NUM_ANIMATIONS; ++i ) + if( !DoesFileExist( GetAnimPath( (Animation) i ) ) ) + return false; + return true; +} + +REGISTER_SCREEN_CLASS( ScreenHowToPlay ); +ScreenHowToPlay::ScreenHowToPlay() +{ + m_iW2s = 0; + m_iNumW2s = NUM_W2S; + + // initialize these because they might not be used. + m_pLifeMeterBar = NULL; + m_pmCharacter = NULL; + m_pmDancePad = NULL; +} + +void ScreenHowToPlay::Init() +{ + ScreenAttract::Init(); + + if( (bool)USE_PAD && DoesFileExist( GetAnimPath(ANIM_DANCE_PAD) ) ) + { + m_pmDancePad = new Model; + m_pmDancePad->SetName( "Pad" ); + m_pmDancePad->LoadMilkshapeAscii( GetAnimPath(ANIM_DANCE_PAD) ); + // xxx: hardcoded rotation. can be undone, but still. -freem + m_pmDancePad->SetRotationX( 35 ); + ActorUtil::LoadAllCommandsAndSetXY( m_pmDancePad, m_sName ); + } + + // Display a character + vector vpCharacters; + CHARMAN->GetCharacters( vpCharacters ); + if( (bool)USE_CHARACTER && vpCharacters.size() && HaveAllCharAnimations() ) + { + Character* displayChar; + if( !CHARACTER_NAME.GetValue().empty() && CHARMAN->GetCharacterFromID(CHARACTER_NAME) ) + displayChar = CHARMAN->GetCharacterFromID(CHARACTER_NAME); + else + displayChar = CHARMAN->GetRandomCharacter(); + + RString sModelPath = displayChar->GetModelPath(); + if( sModelPath != "" ) + { + m_pmCharacter = new Model; + m_pmCharacter->SetName( "Character" ); + m_pmCharacter->LoadMilkshapeAscii( displayChar->GetModelPath() ); + m_pmCharacter->LoadMilkshapeAsciiBones( "Step-LEFT", GetAnimPath( ANIM_LEFT ) ); + m_pmCharacter->LoadMilkshapeAsciiBones( "Step-DOWN", GetAnimPath( ANIM_DOWN ) ); + m_pmCharacter->LoadMilkshapeAsciiBones( "Step-UP", GetAnimPath( ANIM_UP ) ); + m_pmCharacter->LoadMilkshapeAsciiBones( "Step-RIGHT", GetAnimPath( ANIM_RIGHT ) ); + m_pmCharacter->LoadMilkshapeAsciiBones( "Step-JUMPLR", GetAnimPath( ANIM_JUMPLR ) ); + RString sRestFile = displayChar->GetRestAnimationPath(); + ASSERT( !sRestFile.empty() ); + m_pmCharacter->LoadMilkshapeAsciiBones( "rest",displayChar->GetRestAnimationPath() ); + m_pmCharacter->SetDefaultAnimation( "rest" ); + m_pmCharacter->PlayAnimation( "rest" ); // Stay bouncing after a step has finished animating. + + // xxx: hardcoded rotation. can be undone, but still. -freem + m_pmCharacter->SetRotationX( 40 ); + m_pmCharacter->SetCullMode( CULL_NONE ); // many of the models floating around have the vertex order flipped + m_pmCharacter->RunCommands( displayChar->m_cmdInit ); // run InitCommand from file + ActorUtil::LoadAllCommandsAndSetXY( m_pmCharacter, m_sName ); + } + } + + GAMESTATE->SetCurrentStyle( GAMEMAN->GetHowToPlayStyleForGame(GAMESTATE->m_pCurGame) ); + + if( USE_PLAYER ) + { + GAMESTATE->SetMasterPlayerNumber(PLAYER_1); + + m_pLifeMeterBar = new LifeMeterBar; + m_pLifeMeterBar->Load( GAMESTATE->m_pPlayerState[PLAYER_1], &STATSMAN->m_CurStageStats.m_player[PLAYER_1] ); + m_pLifeMeterBar->SetName("LifeMeterBar"); + ActorUtil::LoadAllCommandsAndSetXY( m_pLifeMeterBar, m_sName ); + m_pLifeMeterBar->FillForHowToPlay( NUM_W2S, NUM_MISSES ); + + // Allow themers to use either a .ssc or .sm file for this. -aj + RString sStepsPath = THEME->GetPathO(m_sName, "steps"); + SSCLoader loaderSSC; + SMLoader loaderSM; + if( sStepsPath.Right(4) == ".ssc" ) + loaderSSC.LoadFromSimfile( sStepsPath, m_Song, false ); + else + loaderSM.LoadFromSimfile( sStepsPath, m_Song, false ); + m_Song.AddAutoGenNotes(); + + const Style* pStyle = GAMESTATE->GetCurrentStyle(); + + Steps *pSteps = SongUtil::GetClosestNotes( &m_Song, pStyle->m_StepsType, Difficulty_Beginner ); + ASSERT_M( pSteps != NULL, ssprintf("No playable steps of StepsType '%s' for ScreenHowToPlay", StringConversion::ToString(pStyle->m_StepsType).c_str()) ); + + pSteps->m_Timing.TidyUpData(); + NoteData tempNoteData; + pSteps->GetNoteData( tempNoteData ); + pStyle->GetTransformedNoteDataForStyle( PLAYER_1, tempNoteData, m_NoteData ); + + GAMESTATE->m_pCurSong.Set( &m_Song ); + GAMESTATE->m_pCurSteps[PLAYER_1].Set(pSteps); + GAMESTATE->m_bGameplayLeadIn.Set( false ); + GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerController = PC_AUTOPLAY; + + m_Player->Init( + "Player", + GAMESTATE->m_pPlayerState[PLAYER_1], + NULL, + m_pLifeMeterBar, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL ); + m_Player.Load( m_NoteData ); + m_Player->SetName( "Player" ); + this->AddChild( m_Player ); + ActorUtil::LoadAllCommandsAndSetXY( m_Player, m_sName ); + + // Don't show judgment + PO_GROUP_ASSIGN( GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerOptions, ModsLevel_Stage, m_fBlind, 1.0f ); + GAMESTATE->m_bDemonstrationOrJukebox = true; + } + + // deferred until after the player, so the notes go under it + if( m_pLifeMeterBar ) + this->AddChild( m_pLifeMeterBar ); + if( m_pmDancePad ) + this->AddChild( m_pmDancePad ); + if( m_pmCharacter ) + this->AddChild( m_pmCharacter ); + + m_fFakeSecondsIntoSong = 0; + + this->MoveToTail( &m_In ); + this->MoveToTail( &m_Out ); +} + +ScreenHowToPlay::~ScreenHowToPlay() +{ + delete m_pLifeMeterBar; + delete m_pmCharacter; + delete m_pmDancePad; +} + +void ScreenHowToPlay::Step() +{ +// xxx: assumes dance. -freem +#define ST_LEFT 0x01 +#define ST_DOWN 0x02 +#define ST_UP 0x04 +#define ST_RIGHT 0x08 +#define ST_JUMPLR (ST_LEFT | ST_RIGHT) +#define ST_JUMPUD (ST_UP | ST_DOWN) + + int iStep = 0; + const int iNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat + 0.6f ); + // if we want to miss from here on out, don't process steps. + if( m_iW2s < m_iNumW2s && m_NoteData.IsThereATapAtRow( iNoteRow ) ) + { + const int iNumTracks = m_NoteData.GetNumTracks(); + for( int k=0; kPlayAnimation( "Step-LEFT", 1.8f ); break; + case ST_RIGHT: m_pmCharacter->PlayAnimation( "Step-RIGHT", 1.8f ); break; + case ST_UP: m_pmCharacter->PlayAnimation( "Step-UP", 1.8f ); break; + case ST_DOWN: m_pmCharacter->PlayAnimation( "Step-DOWN", 1.8f ); break; + case ST_JUMPLR: m_pmCharacter->PlayAnimation( "Step-JUMPLR", 1.8f ); break; + case ST_JUMPUD: + // Until I can get an UP+DOWN jump animation, this will have to do. + m_pmCharacter->PlayAnimation( "Step-JUMPLR", 1.8f ); + + m_pmCharacter->StopTweening(); + m_pmCharacter->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /8, TWEEN_LINEAR ); + m_pmCharacter->SetRotationY( 90 ); + m_pmCharacter->BeginTweening( (1/(GAMESTATE->m_Position.m_fCurBPS * 2) ) ); //sleep between jump-frames + m_pmCharacter->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /6, TWEEN_LINEAR ); + m_pmCharacter->SetRotationY( 0 ); + break; + } + } +} + +void ScreenHowToPlay::Update( float fDelta ) +{ + if( GAMESTATE->m_pCurSong != NULL ) + { + RageTimer tm; + GAMESTATE->UpdateSongPosition( m_fFakeSecondsIntoSong, GAMESTATE->m_pCurSong->m_SongTiming, tm, true ); + m_fFakeSecondsIntoSong += fDelta; + + static int iLastNoteRowCounted = 0; + int iCurNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat ); + + if( iCurNoteRow != iLastNoteRowCounted &&m_NoteData.IsThereATapAtRow(iCurNoteRow) ) + { + if( m_pLifeMeterBar && !m_Player ) + { + if ( m_iW2s < m_iNumW2s ) + m_pLifeMeterBar->ChangeLife( TNS_W2 ); + else + m_pLifeMeterBar->ChangeLife( TNS_Miss ); + } + m_iW2s++; + iLastNoteRowCounted = iCurNoteRow; + } + + // Once we hit the number of perfects we want, we want to fail. Switch + // the controller to HUMAN. Since we aren't taking input, the steps will + // always be misses. + if( m_iW2s > m_iNumW2s ) + GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerController = PC_HUMAN; + + // Per the above code, we don't always want the character stepping. + // If they try to make all of the steps in the miss part, they look + // silly. Have then stand still instead. - freem + if ( m_pmCharacter ) + { + if( m_iW2s <= m_iNumW2s ) + Step(); + } + } + + ScreenAttract::Update( fDelta ); +} + +void ScreenHowToPlay::HandleScreenMessage( const ScreenMessage SM ) +{ + if( SM == SM_GainFocus ) + { + // We do this ourself. + SOUND->HandleSongTimer( false ); + } + else if( SM == SM_LoseFocus ) + { + SOUND->HandleSongTimer( true ); + } + else if( SM == SM_GoToNextScreen ) + { + GAMESTATE->Reset(); + } + ScreenAttract::HandleScreenMessage( SM ); +} + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the ScreenHowToPlay. */ +class LunaScreenHowToPlay: public Luna +{ +public: + static int GetLifeMeter( T* p, lua_State *L ) + { + //PlayerNumber pn = Enum::Check( L, 1 ); + + p->m_pLifeMeterBar->PushSelf( L ); + return 1; + } + + LunaScreenHowToPlay() + { + ADD_METHOD( GetLifeMeter ); + } +}; + +LUA_REGISTER_DERIVED_CLASS( ScreenHowToPlay, ScreenAttract ) +// lua end + +/* + * (c) 2001-2004 Chris Danford, Thad Ward + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/ScreenRanking.cpp b/src/ScreenRanking.cpp index af57adc86e..2f63dd08de 100644 --- a/src/ScreenRanking.cpp +++ b/src/ScreenRanking.cpp @@ -1,362 +1,363 @@ -#include "global.h" -#include "ScreenRanking.h" -#include "ScreenManager.h" -#include "SongManager.h" -#include "GameState.h" -#include "GameManager.h" -#include "ProfileManager.h" -#include "Profile.h" - - -static const char *RankingTypeNames[] = { - "Category", - "Course", -}; -XToString( RankingType ); -LuaXType( RankingType ); - -AutoScreenMessage( SM_ShowNextPage ); -AutoScreenMessage( SM_HidePage ); - -REGISTER_SCREEN_CLASS( ScreenRanking ); - -#define BULLET_X(row) (BULLET_START_X+ROW_SPACING_X*row) -#define BULLET_Y(row) (BULLET_START_Y+ROW_SPACING_Y*row) -#define NAME_X(row) (NAME_START_X+ROW_SPACING_X*row) -#define NAME_Y(row) (NAME_START_Y+ROW_SPACING_Y*row) -#define SCORE_X(row) (SCORE_START_X+ROW_SPACING_X*row) -#define SCORE_Y(row) (SCORE_START_Y+ROW_SPACING_Y*row) -#define POINTS_X(row) (POINTS_START_X+ROW_SPACING_X*row) -#define POINTS_Y(row) (POINTS_START_Y+ROW_SPACING_Y*row) -#define TIME_X(row) (TIME_START_X+ROW_SPACING_X*row) -#define TIME_Y(row) (TIME_START_Y+ROW_SPACING_Y*row) - -static RString STEPS_TYPE_COLOR_NAME( size_t i ) { return ssprintf("StepsTypeColor%d",int(i+1)); } - -void ScreenRanking::Init() -{ - STEPS_TYPES_TO_SHOW.Load( m_sName,"StepsTypesToHide" ); // tricky: The metric name is "hide" because ThemeMetricStepsTypesToShow takes a list of what to hide and returns what StepsTypes to show - PAGE_FADE_SECONDS.Load ( m_sName,"PageFadeSeconds" ); - - ScreenAttract::Init(); - - m_textStepsType.SetName( "StepsType" ); - m_textStepsType.LoadFromFont( THEME->GetPathF(m_sName,"StepsType") ); - m_textStepsType.SetShadowLength( 0 ); - this->AddChild( &m_textStepsType ); - LOAD_ALL_COMMANDS( m_textStepsType ); - - - RANKING_TYPE.Load ( m_sName,"RankingType"); - COURSES_TO_SHOW.Load ( m_sName,"CoursesToShow"); - SECONDS_PER_PAGE.Load ( m_sName,"SecondsPerPage" ); - - NO_SCORE_NAME.Load( m_sName,"NoScoreName" ); - ROW_SPACING_X.Load( m_sName,"RowSpacingX" ); - ROW_SPACING_Y.Load( m_sName,"RowSpacingY" ); - BULLET_START_X.Load( m_sName, "BulletStartX" ); - BULLET_START_Y.Load( m_sName, "BulletStartY" ); - NAME_START_X.Load( m_sName, "NameStartX" ); - NAME_START_Y.Load( m_sName, "NameStartY" ); - SCORE_START_X.Load( m_sName, "ScoreStartX" ); - SCORE_START_Y.Load( m_sName, "ScoreStartY" ); - POINTS_START_X.Load( m_sName, "PointsStartX" ); - POINTS_START_Y.Load( m_sName, "PointsStartY" ); - TIME_START_X.Load( m_sName, "TimeStartX" ); - TIME_START_Y.Load( m_sName, "TimeStartY" ); - STEPS_TYPE_COLOR.Load( m_sName,STEPS_TYPE_COLOR_NAME,NUM_RANKING_LINES ); - - if( RANKING_TYPE == RankingType_Category ) - { - m_textCategory.SetName( "Category" ); - m_textCategory.LoadFromFont( THEME->GetPathF(m_sName,"category") ); - //m_textCategory.SetShadowLength( 0 ); - this->AddChild( &m_textCategory ); - LOAD_ALL_COMMANDS( m_textCategory ); - - for( unsigned i=0; iAddChild( &m_Banner ); - LOAD_ALL_COMMANDS( m_Banner ); - - m_textCourseTitle.SetName( "CourseTitle" ); - m_textCourseTitle.LoadFromFont( THEME->GetPathF(m_sName,"course title") ); - //m_textCourseTitle.SetShadowLength( 0 ); - this->AddChild( &m_textCourseTitle ); - LOAD_ALL_COMMANDS( m_textCourseTitle ); - - vector asCoursePaths; - split( COURSES_TO_SHOW, ",", asCoursePaths, true ); - for( unsigned i=0; iGetCourseFromPath( asCoursePaths[c] ); - if( pts.pCourse == NULL ) - continue; - - pts.pTrail = pts.pCourse->GetTrail( st ); - if( pts.pTrail == NULL ) - continue; - - m_vPagesToShow.push_back( pts ); - } - } - } - - for( int l=0; lGetPathG( m_sName, ssprintf("bullets 1x%d",NUM_RANKING_LINES) ) ); - m_sprBullets[l]->SetName( ssprintf("Bullet%d",l+1) ); - m_sprBullets[l]->StopAnimating(); - m_sprBullets[l]->SetState( l ); - m_sprBullets[l]->SetXY( BULLET_X(l), BULLET_Y(l) ); - ActorUtil::LoadAllCommands( *m_sprBullets[l], m_sName ); - this->AddChild( m_sprBullets[l] ); - - m_textNames[l].SetName( ssprintf("Name%d",l+1) ); - m_textNames[l].LoadFromFont( THEME->GetPathF(m_sName,"name") ); - m_textNames[l].SetXY( NAME_X(l), NAME_Y(l) ); - ActorUtil::LoadAllCommands( m_textNames[l], m_sName ); - this->AddChild( &m_textNames[l] ); - - m_textScores[l].SetName( ssprintf("Score%d",l+1) ); - m_textScores[l].LoadFromFont( THEME->GetPathF(m_sName,"score") ); - m_textScores[l].SetXY( SCORE_X(l), SCORE_Y(l) ); - ActorUtil::LoadAllCommands( m_textScores[l], m_sName ); - this->AddChild( &m_textScores[l] ); - - m_textPoints[l].SetName( ssprintf("Points%d",l+1) ); - m_textPoints[l].LoadFromFont( THEME->GetPathF(m_sName,"points") ); - m_textPoints[l].SetVisible( false ); - m_textPoints[l].SetXY( POINTS_X(l), POINTS_Y(l) ); - ActorUtil::LoadAllCommands( m_textPoints[l], m_sName ); - this->AddChild( &m_textPoints[l] ); - - m_textTime[l].SetName( ssprintf("Time%d",l+1) ); - m_textTime[l].LoadFromFont( THEME->GetPathF(m_sName,"time") ); - m_textTime[l].SetVisible( false ); - m_textTime[l].SetXY( TIME_X(l), TIME_Y(l) ); - ActorUtil::LoadAllCommands( m_textTime[l], m_sName ); - this->AddChild( &m_textTime[l] ); - } -} - -void ScreenRanking::BeginScreen() -{ - m_iNextPageToShow = 0; - - ScreenAttract::BeginScreen(); - - this->HandleScreenMessage( SM_ShowNextPage ); -} - -void ScreenRanking::HandleScreenMessage( const ScreenMessage SM ) -{ - if( SM == SM_ShowNextPage ) - { - if( m_iNextPageToShow < m_vPagesToShow.size() ) - { - float fSecsToShow = SetPage( m_vPagesToShow[m_iNextPageToShow] ); - ++m_iNextPageToShow; - this->SortByDrawOrder(); - - this->PostScreenMessage( SM_HidePage, fSecsToShow-PAGE_FADE_SECONDS ); - } - else - { - StartTransitioningScreen(SM_GoToNextScreen); - } - } - else if( SM == SM_HidePage ) - { - this->PlayCommand( "SwitchPage" ); - this->PostScreenMessage( SM_ShowNextPage, PAGE_FADE_SECONDS ); - } - - ScreenAttract::HandleScreenMessage( SM ); -} - -float ScreenRanking::SetPage( const PageToShow &pts ) -{ - // This is going to take a while to load. Possibly longer than one frame. - // So, zero the next update so we don't skip. - SCREENMAN->ZeroNextUpdate(); - - // init page - StepsType st = pts.aTypes.front().second; - m_textStepsType.SetText( GAMEMAN->GetStepsTypeInfo(st).GetLocalizedString() ); - - - bool bShowScores = false; - bool bShowPoints = false; - bool bShowTime = false; - switch( RANKING_TYPE ) - { - case RankingType_Category: - bShowScores = true; - break; - case RankingType_SpecificTrail: - bShowScores = !pts.pCourse->IsOni(); - bShowPoints = pts.pCourse->IsOni(); - bShowTime = pts.pCourse->IsOni(); - break; - default: break; - } - - for( int l=0; lGetMachineProfile()->GetCategoryHighScoreList(st, pts.category); - HighScore hs; - bool bRecentHighScore = false; - if( l < (int)hsl.vHighScores.size() ) - { - hs = hsl.vHighScores[l]; - RString *psName = hsl.vHighScores[l].GetNameMutable(); - bRecentHighScore = find( GAMESTATE->m_vpsNamesThatWereFilled.begin(), GAMESTATE->m_vpsNamesThatWereFilled.end(), psName ) != GAMESTATE->m_vpsNamesThatWereFilled.end(); - } - else - { - hs.SetName( NO_SCORE_NAME ); - } - - m_textNames[l].SetText( hs.GetDisplayName() ); - m_textScores[l].SetText( ssprintf("%09i",hs.GetScore()) ); - m_textNames[l].SetDiffuse( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); - m_textScores[l].SetDiffuse( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); - - if( bRecentHighScore ) - { - m_textNames[l].SetEffectGlowBlink(0.1f); - m_textScores[l].SetEffectGlowBlink(0.1f); - } - else - { - m_textNames[l].StopEffect(); - m_textScores[l].StopEffect(); - } - } - } - return SECONDS_PER_PAGE; - case RankingType_SpecificTrail: - { - m_textCourseTitle.SetText( pts.pCourse->GetDisplayFullTitle() ); - - m_Banner.LoadFromCourse( pts.pCourse ); - - const HighScoreList &hsl = PROFILEMAN->GetMachineProfile()->GetCourseHighScoreList( pts.pCourse, pts.pTrail ); - for( int l=0; lm_vpsNamesThatWereFilled.begin(), GAMESTATE->m_vpsNamesThatWereFilled.end(), psName ) != GAMESTATE->m_vpsNamesThatWereFilled.end(); - } - else - { - hs.SetName( NO_SCORE_NAME ); - } - - m_textNames[l].SetText( hs.GetDisplayName() ); - if( pts.pCourse->IsOni() ) - { - m_textPoints[l].SetText( ssprintf("%04d",hs.GetScore()) ); - m_textTime[l].SetText( SecondsToMMSSMsMs(hs.GetSurviveSeconds()) ); - m_textScores[l].SetText( "" ); - } else { - m_textPoints[l].SetText( "" ); - m_textTime[l].SetText( "" ); - m_textScores[l].SetText( ssprintf("%09d",hs.GetScore()) ); - } - m_textNames[l].SetDiffuse( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); - m_textPoints[l].SetDiffuse( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); - m_textTime[l].SetDiffuse( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); - m_textScores[l].SetDiffuse( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); - - if( bRecentHighScore ) - { - m_textNames[l].SetEffectGlowBlink(0.1f); - m_textScores[l].SetEffectGlowBlink(0.1f); - } - else - { - m_textNames[l].StopEffect(); - m_textScores[l].StopEffect(); - } - } - } - return SECONDS_PER_PAGE; - default: - ASSERT(0); - return 0; - } -} - -/* - * (c) 2001-2007 Chris Danford, Glenn Maynard - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#include "global.h" +#include "ScreenRanking.h" +#include "ScreenManager.h" +#include "SongManager.h" +#include "GameState.h" +#include "GameManager.h" +#include "ProfileManager.h" +#include "Profile.h" + + +static const char *RankingTypeNames[] = { + "Category", + "Course", +}; +XToString( RankingType ); +LuaXType( RankingType ); + +AutoScreenMessage( SM_ShowNextPage ); +AutoScreenMessage( SM_HidePage ); + +REGISTER_SCREEN_CLASS( ScreenRanking ); + +#define BULLET_X(row) (BULLET_START_X+ROW_SPACING_X*row) +#define BULLET_Y(row) (BULLET_START_Y+ROW_SPACING_Y*row) +#define NAME_X(row) (NAME_START_X+ROW_SPACING_X*row) +#define NAME_Y(row) (NAME_START_Y+ROW_SPACING_Y*row) +#define SCORE_X(row) (SCORE_START_X+ROW_SPACING_X*row) +#define SCORE_Y(row) (SCORE_START_Y+ROW_SPACING_Y*row) +#define POINTS_X(row) (POINTS_START_X+ROW_SPACING_X*row) +#define POINTS_Y(row) (POINTS_START_Y+ROW_SPACING_Y*row) +#define TIME_X(row) (TIME_START_X+ROW_SPACING_X*row) +#define TIME_Y(row) (TIME_START_Y+ROW_SPACING_Y*row) + +static RString STEPS_TYPE_COLOR_NAME( size_t i ) { return ssprintf("StepsTypeColor%d",int(i+1)); } + +void ScreenRanking::Init() +{ + STEPS_TYPES_TO_SHOW.Load( m_sName,"StepsTypesToHide" ); // tricky: The metric name is "hide" because ThemeMetricStepsTypesToShow takes a list of what to hide and returns what StepsTypes to show + PAGE_FADE_SECONDS.Load ( m_sName,"PageFadeSeconds" ); + + ScreenAttract::Init(); + + m_textStepsType.SetName( "StepsType" ); + m_textStepsType.LoadFromFont( THEME->GetPathF(m_sName,"StepsType") ); + m_textStepsType.SetShadowLength( 0 ); + this->AddChild( &m_textStepsType ); + LOAD_ALL_COMMANDS( m_textStepsType ); + + + RANKING_TYPE.Load ( m_sName,"RankingType"); + COURSES_TO_SHOW.Load ( m_sName,"CoursesToShow"); + SECONDS_PER_PAGE.Load ( m_sName,"SecondsPerPage" ); + + NO_SCORE_NAME.Load( m_sName,"NoScoreName" ); + ROW_SPACING_X.Load( m_sName,"RowSpacingX" ); + ROW_SPACING_Y.Load( m_sName,"RowSpacingY" ); + BULLET_START_X.Load( m_sName, "BulletStartX" ); + BULLET_START_Y.Load( m_sName, "BulletStartY" ); + NAME_START_X.Load( m_sName, "NameStartX" ); + NAME_START_Y.Load( m_sName, "NameStartY" ); + SCORE_START_X.Load( m_sName, "ScoreStartX" ); + SCORE_START_Y.Load( m_sName, "ScoreStartY" ); + POINTS_START_X.Load( m_sName, "PointsStartX" ); + POINTS_START_Y.Load( m_sName, "PointsStartY" ); + TIME_START_X.Load( m_sName, "TimeStartX" ); + TIME_START_Y.Load( m_sName, "TimeStartY" ); + STEPS_TYPE_COLOR.Load( m_sName,STEPS_TYPE_COLOR_NAME,NUM_RANKING_LINES ); + + if( RANKING_TYPE == RankingType_Category ) + { + m_textCategory.SetName( "Category" ); + m_textCategory.LoadFromFont( THEME->GetPathF(m_sName,"category") ); + //m_textCategory.SetShadowLength( 0 ); + this->AddChild( &m_textCategory ); + LOAD_ALL_COMMANDS( m_textCategory ); + + for( unsigned i=0; iAddChild( &m_Banner ); + LOAD_ALL_COMMANDS( m_Banner ); + + m_textCourseTitle.SetName( "CourseTitle" ); + m_textCourseTitle.LoadFromFont( THEME->GetPathF(m_sName,"course title") ); + //m_textCourseTitle.SetShadowLength( 0 ); + this->AddChild( &m_textCourseTitle ); + LOAD_ALL_COMMANDS( m_textCourseTitle ); + + vector asCoursePaths; + split( COURSES_TO_SHOW, ",", asCoursePaths, true ); + for( unsigned i=0; iGetCourseFromPath( asCoursePaths[c] ); + if( pts.pCourse == NULL ) + continue; + + pts.pTrail = pts.pCourse->GetTrail( st ); + if( pts.pTrail == NULL ) + continue; + + m_vPagesToShow.push_back( pts ); + } + } + } + + for( int l=0; lGetPathG( m_sName, ssprintf("bullets 1x%d",NUM_RANKING_LINES) ) ); + m_sprBullets[l]->SetName( ssprintf("Bullet%d",l+1) ); + m_sprBullets[l]->StopAnimating(); + m_sprBullets[l]->SetState( l ); + m_sprBullets[l]->SetXY( BULLET_X(l), BULLET_Y(l) ); + ActorUtil::LoadAllCommands( *m_sprBullets[l], m_sName ); + this->AddChild( m_sprBullets[l] ); + + m_textNames[l].SetName( ssprintf("Name%d",l+1) ); + m_textNames[l].LoadFromFont( THEME->GetPathF(m_sName,"name") ); + m_textNames[l].SetXY( NAME_X(l), NAME_Y(l) ); + ActorUtil::LoadAllCommands( m_textNames[l], m_sName ); + this->AddChild( &m_textNames[l] ); + + m_textScores[l].SetName( ssprintf("Score%d",l+1) ); + m_textScores[l].LoadFromFont( THEME->GetPathF(m_sName,"score") ); + m_textScores[l].SetXY( SCORE_X(l), SCORE_Y(l) ); + ActorUtil::LoadAllCommands( m_textScores[l], m_sName ); + this->AddChild( &m_textScores[l] ); + + m_textPoints[l].SetName( ssprintf("Points%d",l+1) ); + m_textPoints[l].LoadFromFont( THEME->GetPathF(m_sName,"points") ); + m_textPoints[l].SetVisible( false ); + m_textPoints[l].SetXY( POINTS_X(l), POINTS_Y(l) ); + ActorUtil::LoadAllCommands( m_textPoints[l], m_sName ); + this->AddChild( &m_textPoints[l] ); + + m_textTime[l].SetName( ssprintf("Time%d",l+1) ); + m_textTime[l].LoadFromFont( THEME->GetPathF(m_sName,"time") ); + m_textTime[l].SetVisible( false ); + m_textTime[l].SetXY( TIME_X(l), TIME_Y(l) ); + ActorUtil::LoadAllCommands( m_textTime[l], m_sName ); + this->AddChild( &m_textTime[l] ); + } +} + +void ScreenRanking::BeginScreen() +{ + m_iNextPageToShow = 0; + + ScreenAttract::BeginScreen(); + + this->HandleScreenMessage( SM_ShowNextPage ); +} + +void ScreenRanking::HandleScreenMessage( const ScreenMessage SM ) +{ + if( SM == SM_ShowNextPage ) + { + if( m_iNextPageToShow < m_vPagesToShow.size() ) + { + float fSecsToShow = SetPage( m_vPagesToShow[m_iNextPageToShow] ); + ++m_iNextPageToShow; + this->SortByDrawOrder(); + + this->PlayCommand( "NextPage" ); + this->PostScreenMessage( SM_HidePage, fSecsToShow-PAGE_FADE_SECONDS ); + } + else + { + StartTransitioningScreen(SM_GoToNextScreen); + } + } + else if( SM == SM_HidePage ) + { + this->PlayCommand( "SwitchedPage" ); + this->PostScreenMessage( SM_ShowNextPage, PAGE_FADE_SECONDS ); + } + + ScreenAttract::HandleScreenMessage( SM ); +} + +float ScreenRanking::SetPage( const PageToShow &pts ) +{ + // This is going to take a while to load. Possibly longer than one frame. + // So, zero the next update so we don't skip. + SCREENMAN->ZeroNextUpdate(); + + // init page + StepsType st = pts.aTypes.front().second; + m_textStepsType.SetText( GAMEMAN->GetStepsTypeInfo(st).GetLocalizedString() ); + + + bool bShowScores = false; + bool bShowPoints = false; + bool bShowTime = false; + switch( RANKING_TYPE ) + { + case RankingType_Category: + bShowScores = true; + break; + case RankingType_SpecificTrail: + bShowScores = !pts.pCourse->IsOni(); + bShowPoints = pts.pCourse->IsOni(); + bShowTime = pts.pCourse->IsOni(); + break; + default: break; + } + + for( int l=0; lGetMachineProfile()->GetCategoryHighScoreList(st, pts.category); + HighScore hs; + bool bRecentHighScore = false; + if( l < (int)hsl.vHighScores.size() ) + { + hs = hsl.vHighScores[l]; + RString *psName = hsl.vHighScores[l].GetNameMutable(); + bRecentHighScore = find( GAMESTATE->m_vpsNamesThatWereFilled.begin(), GAMESTATE->m_vpsNamesThatWereFilled.end(), psName ) != GAMESTATE->m_vpsNamesThatWereFilled.end(); + } + else + { + hs.SetName( NO_SCORE_NAME ); + } + + m_textNames[l].SetText( hs.GetDisplayName() ); + m_textScores[l].SetText( ssprintf("%09i",hs.GetScore()) ); + m_textNames[l].SetDiffuseColor( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); + m_textScores[l].SetDiffuseColor( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); + + if( bRecentHighScore ) + { + m_textNames[l].SetEffectGlowBlink(0.1f); + m_textScores[l].SetEffectGlowBlink(0.1f); + } + else + { + m_textNames[l].StopEffect(); + m_textScores[l].StopEffect(); + } + } + } + return SECONDS_PER_PAGE; + case RankingType_SpecificTrail: + { + m_textCourseTitle.SetText( pts.pCourse->GetDisplayFullTitle() ); + + m_Banner.LoadFromCourse( pts.pCourse ); + + const HighScoreList &hsl = PROFILEMAN->GetMachineProfile()->GetCourseHighScoreList( pts.pCourse, pts.pTrail ); + for( int l=0; lm_vpsNamesThatWereFilled.begin(), GAMESTATE->m_vpsNamesThatWereFilled.end(), psName ) != GAMESTATE->m_vpsNamesThatWereFilled.end(); + } + else + { + hs.SetName( NO_SCORE_NAME ); + } + + m_textNames[l].SetText( hs.GetDisplayName() ); + if( pts.pCourse->IsOni() ) + { + m_textPoints[l].SetText( ssprintf("%04d",hs.GetScore()) ); + m_textTime[l].SetText( SecondsToMMSSMsMs(hs.GetSurviveSeconds()) ); + m_textScores[l].SetText( "" ); + } else { + m_textPoints[l].SetText( "" ); + m_textTime[l].SetText( "" ); + m_textScores[l].SetText( ssprintf("%09d",hs.GetScore()) ); + } + m_textNames[l].SetDiffuseColor( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); + m_textPoints[l].SetDiffuseColor( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); + m_textTime[l].SetDiffuseColor( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); + m_textScores[l].SetDiffuseColor( STEPS_TYPE_COLOR.GetValue(pts.colorIndex) ); + + if( bRecentHighScore ) + { + m_textNames[l].SetEffectGlowBlink(0.1f); + m_textScores[l].SetEffectGlowBlink(0.1f); + } + else + { + m_textNames[l].StopEffect(); + m_textScores[l].StopEffect(); + } + } + } + return SECONDS_PER_PAGE; + default: + ASSERT(0); + return 0; + } +} + +/* + * (c) 2001-2007 Chris Danford, Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/Song.cpp b/src/Song.cpp index 719509a18a..d7be9ab51f 100644 --- a/src/Song.cpp +++ b/src/Song.cpp @@ -1,2038 +1,2045 @@ -#include "global.h" -#include "Song.h" -#include "Steps.h" -#include "RageUtil.h" -#include "RageLog.h" -#include "NoteData.h" -#include "RageSoundReader_FileReader.h" -#include "RageSurface_Load.h" -#include "SongCacheIndex.h" -#include "GameManager.h" -#include "PrefsManager.h" -#include "Style.h" -#include "FontCharAliases.h" -#include "TitleSubstitution.h" -#include "BannerCache.h" -//#include "BackgroundCache.h" -#include "Sprite.h" -#include "RageFileManager.h" -#include "RageSurface.h" -#include "NoteDataUtil.h" -#include "SongUtil.h" -#include "StepsUtil.h" -#include "Foreach.h" -#include "BackgroundUtil.h" -#include "SpecialFiles.h" -#include "NotesLoader.h" -#include "NotesLoaderSM.h" -#include "NotesLoaderSSC.h" -#include "NotesWriterDWI.h" -#include "NotesWriterSM.h" -#include "NotesWriterSSC.h" -#include "UnlockManager.h" -#include "LyricsLoader.h" - -#include -#include -#include - - -/** - * @brief The internal version of the cache for StepMania. - * - * Increment this value to invalidate the current cache. */ -const int FILE_CACHE_VERSION = 208; - -/** @brief How long does a song sample last by default? */ -const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f; - -static Preference g_fLongVerSongSeconds( "LongVerSongSeconds", 60*2.5f ); -static Preference g_fMarathonVerSongSeconds( "MarathonVerSongSeconds", 60*5.f ); -static Preference g_BackUpAllSongSaves( "BackUpAllSongSaves", false ); - -static const char *InstrumentTrackNames[] = { - "Guitar", - "Rhythm", - "Bass", -}; -XToString( InstrumentTrack ); -StringToX( InstrumentTrack ); - -Song::Song() -{ - FOREACH_BackgroundLayer( i ) - m_BackgroundChanges[i] = AutoPtrCopyOnWrite(new VBackgroundChange); - m_ForegroundChanges = AutoPtrCopyOnWrite(new VBackgroundChange); - - m_LoadedFromProfile = ProfileSlot_Invalid; - m_fVersion = STEPFILE_VERSION_NUMBER; - m_fMusicSampleStartSeconds = -1; - m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; - m_fMusicLengthSeconds = 0; - firstSecond = -1; - lastSecond = -1; - specifiedLastSecond = -1; - m_SelectionDisplay = SHOW_ALWAYS; - m_bEnabled = true; - m_DisplayBPMType = DISPLAY_BPM_ACTUAL; - m_fSpecifiedBPMMin = 0; - m_fSpecifiedBPMMax = 0; - m_bIsSymLink = false; - m_bHasMusic = false; - m_bHasBanner = false; - m_bHasBackground = false; -} - -Song::~Song() -{ - FOREACH( Steps*, m_vpSteps, s ) - SAFE_DELETE( *s ); - m_vpSteps.clear(); - - // It's the responsibility of the owner of this Song to make sure - // that all pointers to this Song and its Steps are invalidated. -} - -void Song::DetachSteps() -{ - m_vpSteps.clear(); - FOREACH_ENUM( StepsType, st ) - m_vpStepsByType[st].clear(); -} - -float Song::GetFirstSecond() const -{ - return this->firstSecond; -} - -float Song::GetFirstBeat() const -{ - return this->m_SongTiming.GetBeatFromElapsedTime(this->firstSecond); -} - -float Song::GetLastSecond() const -{ - return this->lastSecond; -} - -float Song::GetLastBeat() const -{ - return this->m_SongTiming.GetBeatFromElapsedTime(this->lastSecond); -} - -float Song::GetSpecifiedLastSecond() const -{ - return this->specifiedLastSecond; -} - -float Song::GetSpecifiedLastBeat() const -{ - return this->m_SongTiming.GetBeatFromElapsedTime(this->specifiedLastSecond); -} - -void Song::SetFirstSecond(const float f) -{ - this->firstSecond = f; -} - -void Song::SetLastSecond(const float f) -{ - this->lastSecond = f; -} - -void Song::SetSpecifiedLastSecond(const float f) -{ - this->specifiedLastSecond = f; -} - -// Reset to an empty song. -void Song::Reset() -{ - FOREACH( Steps*, m_vpSteps, s ) - SAFE_DELETE( *s ); - m_vpSteps.clear(); - FOREACH_ENUM( StepsType, st ) - m_vpStepsByType[st].clear(); - - Song empty; - *this = empty; - - // It's the responsibility of the owner of this Song to make sure - // that all pointers to this Song and its Steps are invalidated. -} - - -void Song::AddBackgroundChange( BackgroundLayer iLayer, BackgroundChange seg ) -{ - // Delete old background change at this start beat, if any. - FOREACH( BackgroundChange, GetBackgroundChanges(iLayer), bgc ) - { - if( bgc->m_fStartBeat == seg.m_fStartBeat ) - { - GetBackgroundChanges(iLayer).erase( bgc ); - break; - } - } - - ASSERT( iLayer >= 0 && iLayer < NUM_BackgroundLayer ); - BackgroundUtil::AddBackgroundChange( GetBackgroundChanges(iLayer), seg ); -} - -void Song::AddForegroundChange( BackgroundChange seg ) -{ - BackgroundUtil::AddBackgroundChange( GetForegroundChanges(), seg ); -} - -void Song::AddLyricSegment( LyricSegment seg ) -{ - m_LyricSegments.push_back( seg ); -} - -Steps *Song::CreateSteps() -{ - Steps *pSteps = new Steps; - InitSteps( pSteps ); - return pSteps; -} - -void Song::InitSteps(Steps *pSteps) -{ - pSteps->m_Timing = this->m_SongTiming; - pSteps->m_sAttackString = this->m_sAttackString; - pSteps->m_Attacks = this->m_Attacks; - pSteps->SetDisplayBPM(this->m_DisplayBPMType); - pSteps->SetMinBPM(this->m_fSpecifiedBPMMin); - pSteps->SetMaxBPM(this->m_fSpecifiedBPMMax); -} - -void Song::GetDisplayBpms( DisplayBpms &AddTo ) const -{ - if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) - { - AddTo.Add( m_fSpecifiedBPMMin ); - AddTo.Add( m_fSpecifiedBPMMax ); - } - else - { - float fMinBPM, fMaxBPM; - m_SongTiming.GetActualBPM( fMinBPM, fMaxBPM ); - AddTo.Add( fMinBPM ); - AddTo.Add( fMaxBPM ); - } -} - -const BackgroundChange &Song::GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const -{ - for( unsigned i=0; i fBeat ) - return GetBackgroundChanges(iLayer)[i]; - return GetBackgroundChanges(iLayer)[0]; -} - - -RString Song::GetCacheFilePath() const -{ - return SongCacheIndex::GetCacheFilePath( "Songs", m_sSongDir ); -} - -// Get a path to the SM containing data for this song. It might be a cache file. -const RString &Song::GetSongFilePath() const -{ - ASSERT_M( !m_sSongFileName.empty(), - ssprintf("The song %s has no filename associated with it!", - this->m_sMainTitle.c_str())); - return m_sSongFileName; -} - -/* Hack: This should be a parameter to TidyUpData, but I don't want to pull in - * into Song.h, which is heavily used. */ -static set BlacklistedImages; - -/* If PREFSMAN->m_bFastLoad is true, always load from cache if possible. - * Don't read the contents of sDir if we can avoid it. That means we can't call - * HasMusic(), HasBanner() or GetHashForDirectory(). - * If true, check the directory hash and reload the song from scratch if it's changed. - */ -bool Song::LoadFromSongDir( RString sDir ) -{ -// LOG->Trace( "Song::LoadFromSongDir(%s)", sDir.c_str() ); - ASSERT_M( sDir != "", "Songs can't be loaded from an empty directory!" ); - - // make sure there is a trailing slash at the end of sDir - if( sDir.Right(1) != "/" ) - sDir += "/"; - - // save song dir - m_sSongDir = sDir; - - // save group name - vector sDirectoryParts; - split( m_sSongDir, "/", sDirectoryParts, false ); - ASSERT( sDirectoryParts.size() >= 4 ); /* e.g. "/Songs/Slow/Taps/" */ - m_sGroupName = sDirectoryParts[sDirectoryParts.size()-3]; // second from last item - ASSERT( m_sGroupName != "" ); - - // First, look in the cache for this song (without loading NoteData) - unsigned uCacheHash = SONGINDEX->GetCacheHash(m_sSongDir); - bool bUseCache = true; - RString sCacheFilePath = GetCacheFilePath(); - - if( !DoesFileExist(sCacheFilePath) ) - bUseCache = false; - if( !PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash ) - bUseCache = false; // this cache is out of date - - if( bUseCache ) - { - /* - LOG->Trace("Loading '%s' from cache file '%s'.", - m_sSongDir.c_str(), - GetCacheFilePath().c_str()); - */ - SSCLoader loaderSSC; - bool bLoadedFromSSC = loaderSSC.LoadFromSimfile( sCacheFilePath, *this, true ); - if( !bLoadedFromSSC ) - { - // load from .sm - SMLoader loaderSM; - loaderSM.LoadFromSimfile( sCacheFilePath, *this, true ); - loaderSM.TidyUpData( *this, true ); - } - } - else - { - // There was no entry in the cache for this song, or it was out of date. - // Let's load it from a file, then write a cache entry. - - if( !NotesLoader::LoadFromDir(sDir, *this, BlacklistedImages) ) - { - LOG->UserLog( "Song", sDir, "has no SSC, SM, SMA, DWI, BMS, or KSF files." ); - - vector vs; - GetDirListing( sDir + "*.mp3", vs, false, false ); - GetDirListing( sDir + "*.oga", vs, false, false ); - GetDirListing( sDir + "*.ogg", vs, false, false ); - bool bHasMusic = !vs.empty(); - - if( !bHasMusic ) - { - LOG->UserLog( "Song", sDir, "has no music file either. Ignoring this song directory." ); - return false; - } - // Make sure we have a future filename figured out. - vector folders; - split(sDir, "/", folders); - RString songName = folders[2] + ".ssc"; - this->m_sSongFileName = sDir + songName; - // Continue on with a blank Song so that people can make adjustments using the editor. - } - TidyUpData(false, true); - - // save a cache file so we don't have to parse it all over again next time - if( !SaveToCacheFile() ) - sCacheFilePath = RString(); - } - - FOREACH( Steps*, m_vpSteps, s ) - { - /* Compress all Steps. During initial caching, this will remove cached - * NoteData; during cached loads, this will just remove cached SMData. */ - (*s)->Compress(); - } - - // Load the cached banners, if it's not loaded already. - if( PREFSMAN->m_BannerCache == BNCACHE_LOW_RES_PRELOAD && m_bHasBanner ) - BANNERCACHE->LoadBanner( GetBannerPath() ); - // Load the cached background, if it's not loaded already. - /* - if( PREFSMAN->m_BackgroundCache == BGCACHE_LOW_RES_PRELOAD && m_bHasBackground ) - BACKGROUNDCACHE->LoadBackground( GetBackgroundPath() ); - */ - - // Add AutoGen pointers. (These aren't cached.) - AddAutoGenNotes(); - - if( !m_bHasMusic ) - { - LOG->UserLog( "Song", sDir, "has no music; ignored." ); - return false; // don't load this song - } - - return true; // do load this song -} - -bool Song::ReloadFromSongDir( RString sDir ) -{ - RemoveAutoGenNotes(); - vector vOldSteps = m_vpSteps; - - Song copy; - if( !copy.LoadFromSongDir( sDir ) ) - return false; - copy.RemoveAutoGenNotes(); - *this = copy; - - // First we assemble a map to let us easily find the new steps - map mNewSteps; - for( vector::const_iterator it = m_vpSteps.begin(); it != m_vpSteps.end(); ++it ) - { - StepsID id; - id.FromSteps( *it ); - mNewSteps[id] = *it; - } - - // Now we wipe out the new pointers, which were shallow copied and not deep copied... - m_vpSteps.clear(); - FOREACH_ENUM( StepsType, i ) - m_vpStepsByType[i].clear(); - - /* Then we copy as many Steps as possible on top of the old pointers. - * The only pointers that change are pointers to Steps that are not in the - * reverted file, which we delete, and pointers to Steps that are in the - * reverted file but not the original *this, which we create new copies of. - * We have to go through these hoops because many places assume the Steps - * pointers don't change - even though there are other ways they can change, - * such as deleting a Steps via the editor. */ - for( vector::const_iterator itOld = vOldSteps.begin(); itOld != vOldSteps.end(); ++itOld ) - { - StepsID id; - id.FromSteps( *itOld ); - map::iterator itNew = mNewSteps.find( id ); - if( itNew == mNewSteps.end() ) - { - // This stepchart didn't exist in the file we reverted from - delete *itOld; - } - else - { - Steps *OldSteps = *itOld; - *OldSteps = *(itNew->second); - AddSteps( OldSteps ); - mNewSteps.erase( itNew ); - } - } - // The leftovers in the map are steps that didn't exist before we reverted - for( map::const_iterator it = mNewSteps.begin(); it != mNewSteps.end(); ++it ) - { - Steps *NewSteps = new Steps(); - *NewSteps = *(it->second); - AddSteps( NewSteps ); - } - - AddAutoGenNotes(); - return true; -} - -static void GetImageDirListing( RString sPath, vector &AddTo ) -{ - GetDirListing( sPath + ".png", AddTo, false, false ); - GetDirListing( sPath + ".jpg", AddTo, false, false ); - GetDirListing( sPath + ".jpeg", AddTo, false, false ); - GetDirListing( sPath + ".bmp", AddTo, false, false ); - GetDirListing( sPath + ".gif", AddTo, false, false ); -} - -/* Fix up song paths. If there's a leading "./", be sure to keep it: it's - * a signal that the path is from the root directory, not the song directory. - * Other than a leading "./", song paths must never contain "." or "..". */ -void FixupPath( RString &path, const RString &sSongPath ) -{ - // Replace backslashes with slashes in all paths. - FixSlashesInPlace( path ); - - /* Many imported files contain erroneous whitespace before or after - * filenames. Paths usually don't actually start or end with spaces, - * so let's just remove it. */ - Trim( path ); -} - -// Songs in BlacklistImages will never be autodetected as song images. -void Song::TidyUpData( bool fromCache, bool duringCache ) -{ - // We need to do this before calling any of HasMusic, HasHasCDTitle, etc. - ASSERT_M( m_sSongDir.Left(3) != "../", m_sSongDir ); // meaningless - FixupPath( m_sSongDir, "" ); - FixupPath( m_sMusicFile, m_sSongDir ); - FOREACH_ENUM( InstrumentTrack, i ) - if( !m_sInstrumentTrackFile[i].empty() ) - FixupPath( m_sInstrumentTrackFile[i], m_sSongDir ); - FixupPath( m_sBannerFile, m_sSongDir ); - FixupPath( m_sJacketFile, m_sSongDir ); - FixupPath( m_sCDFile, m_sSongDir ); - FixupPath( m_sDiscFile, m_sSongDir ); - FixupPath( m_sLyricsFile, m_sSongDir ); - FixupPath( m_sBackgroundFile, m_sSongDir ); - FixupPath( m_sCDTitleFile, m_sSongDir ); - - if( !HasMusic() ) - { - vector arrayPossibleMusic; - GetDirListing( m_sSongDir + RString("*.mp3"), arrayPossibleMusic ); - GetDirListing( m_sSongDir + RString("*.oga"), arrayPossibleMusic ); - GetDirListing( m_sSongDir + RString("*.ogg"), arrayPossibleMusic ); - GetDirListing( m_sSongDir + RString("*.wav"), arrayPossibleMusic ); - - if( !arrayPossibleMusic.empty() ) - { - int idx = 0; - /* If the first song is "intro", and we have more than one available, - * don't use it--it's probably a KSF intro music file, which we don't - * (yet) support. */ - if( arrayPossibleMusic.size() > 1 && - !arrayPossibleMusic[0].Left(5).CompareNoCase("intro") ) - ++idx; - - // we found a match - m_sMusicFile = arrayPossibleMusic[idx]; - } - } - - // This must be done before radar calculation. - if( HasMusic() ) - { - RString error; - RageSoundReader *Sample = RageSoundReader_FileReader::OpenFile( GetMusicPath(), error ); - /* XXX: Checking if the music file exists eliminates a warning - * originating from BMS files (which have no music file, per se) - * but it's something of a hack. */ - if( Sample == NULL && m_sMusicFile != "" ) - { - LOG->UserLog( "Sound file", GetMusicPath(), "couldn't be opened: %s", error.c_str() ); - - // Don't use this file. - m_sMusicFile = ""; - } - else if ( Sample != NULL ) - { - m_fMusicLengthSeconds = Sample->GetLength() / 1000.0f; - delete Sample; - - if( m_fMusicLengthSeconds < 0 ) - { - // It failed; bad file or something. It's already logged a warning. - m_fMusicLengthSeconds = 100; // guess - } - else if( m_fMusicLengthSeconds == 0 ) - { - LOG->UserLog( "Sound file", GetMusicPath(), "is empty." ); - } - } - } - else // ! HasMusic() - { - m_fMusicLengthSeconds = 100; // guess - LOG->UserLog("Song", - GetSongDir(), - "has no music file; guessing at %f seconds", - m_fMusicLengthSeconds); - } - - if( m_fMusicLengthSeconds < 0 ) - { - LOG->UserLog("Sound file", - GetMusicPath(), - "has a negative length %f.", - m_fMusicLengthSeconds); - m_fMusicLengthSeconds = 0; - } - - m_SongTiming.TidyUpData(); - - FOREACH( Steps *, m_vpSteps, s ) - { - (*s)->m_Timing.TidyUpData(); - } - - /* Generate these before we autogen notes, so the new notes can inherit - * their source's values. */ - ReCalculateRadarValuesAndLastSecond( fromCache, true ); - - Trim( m_sMainTitle ); - Trim( m_sSubTitle ); - Trim( m_sArtist ); - - // Fall back on the song directory name. - if( m_sMainTitle == "" ) - NotesLoader::GetMainAndSubTitlesFromFullTitle(Basename(this->GetSongDir()), - m_sMainTitle, m_sSubTitle ); - - if( m_sArtist == "" ) - m_sArtist = "Unknown artist"; - TranslateTitles(); - - if( m_fMusicSampleStartSeconds == -1 || - m_fMusicSampleLengthSeconds == 0 || - m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) - { - const TimingData &timing = this->m_SongTiming; - m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( 100 ); - - if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) - { - // Attempt to get a reasonable default. - int iBeat = lrintf(this->m_SongTiming.GetBeatFromElapsedTime(this->GetLastSecond())/2); - iBeat -= iBeat%4; - m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( (float)iBeat ); - } - } - - // The old logic meant that you couldn't have sample lengths that go forever, - // e.g. those in Donkey Konga. I never liked that. -freem - if( m_fMusicSampleLengthSeconds <= 0.00f ) - m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; - - // Here's the problem: We have a directory full of images. We want to - // determine which image is the banner, which is the background, and which - // is the CDTitle. - - CHECKPOINT_M( "Looking for images..." ); - - if( !fromCache ) - { - - // First, check the file name for hints. - if( !HasBanner() ) - { - /* If a nonexistant banner file is specified, and we can't find a - * replacement, don't wipe out the old value. */ - //m_sBannerFile = ""; - - // find an image with "banner" in the file name - vector arrayPossibleBanners; - GetImageDirListing( m_sSongDir + "*banner*", arrayPossibleBanners ); - - /* Some people do things differently for the sake of being different. - * Don't match eg. abnormal, numbness. */ - GetImageDirListing( m_sSongDir + "* BN", arrayPossibleBanners ); - - if( !arrayPossibleBanners.empty() ) - m_sBannerFile = arrayPossibleBanners[0]; - } - - if( !HasBackground() ) - { - //m_sBackgroundFile = ""; - - // find an image with "bg" or "background" in the file name - vector arrayPossibleBGs; - GetImageDirListing( m_sSongDir + "*background*", arrayPossibleBGs ); - // don't match e.g. "subgroup", "hobgoblin", etc. - GetImageDirListing( m_sSongDir + "*bg", arrayPossibleBGs ); - if( !arrayPossibleBGs.empty() ) - m_sBackgroundFile = arrayPossibleBGs[0]; - } - - if( !HasJacket() ) - { - // find an image with "jacket" or "albumart" in the filename. - vector arrayPossibleJackets; - GetImageDirListing( m_sSongDir + "jk_*", arrayPossibleJackets ); - GetImageDirListing( m_sSongDir + "*jacket*", arrayPossibleJackets ); - GetImageDirListing( m_sSongDir + "*albumart*", arrayPossibleJackets ); - if( !arrayPossibleJackets.empty() ) - m_sJacketFile = arrayPossibleJackets[0]; - } - - if( !HasCDImage() ) - { - // CD image, a la ddr 1st-3rd (not to be confused with CDTitles) - // find an image with "-cd" at the end of the filename. - vector arrayPossibleCDImages; - GetImageDirListing( m_sSongDir + "*-cd", arrayPossibleCDImages ); - if( !arrayPossibleCDImages.empty() ) - m_sCDFile = arrayPossibleCDImages[0]; - } - - if( !HasDisc() ) - { - // a rectangular graphic, not to be confused with CDImage above. - vector arrayPossibleDiscImages; - GetImageDirListing( m_sSongDir + "* disc", arrayPossibleDiscImages ); - GetImageDirListing( m_sSongDir + "* title", arrayPossibleDiscImages ); - if( !arrayPossibleDiscImages.empty() ) - m_sDiscFile = arrayPossibleDiscImages[0]; - } - - if( !HasCDTitle() ) - { - // find an image with "cdtitle" in the file name - vector arrayPossibleCDTitles; - GetImageDirListing( m_sSongDir + "*cdtitle*", arrayPossibleCDTitles ); - if( !arrayPossibleCDTitles.empty() ) - m_sCDTitleFile = arrayPossibleCDTitles[0]; - } - - if( !HasLyrics() ) - { - // Check if there is a lyric file in here - vector arrayLyricFiles; - GetDirListing(m_sSongDir + RString("*.lrc"), arrayLyricFiles ); - if( !arrayLyricFiles.empty() ) - m_sLyricsFile = arrayLyricFiles[0]; - } - - /* Now, For the images we still haven't found, - * look at the image dimensions of the remaining unclassified images. */ - vector arrayImages; - GetImageDirListing( m_sSongDir + "*", arrayImages ); - - for( unsigned i=0; iUserLog( "Graphic file", sPath, "couldn't be loaded: %s", error.c_str() ); - continue; - } - - const int width = img->w; - const int height = img->h; - delete img; - - if( !HasBackground() && width >= 320 && height >= 240 ) - { - m_sBackgroundFile = arrayImages[i]; - continue; - } - - if( !HasBanner() && 100<=width && width<=320 && 50<=height && height<=240 ) - { - m_sBannerFile = arrayImages[i]; - continue; - } - - /* Some songs have overlarge banners. Check if the ratio is reasonable - * (over 2:1; usually over 3:1), and large (not a cdtitle). */ - if( !HasBanner() && width > 200 && float(width) / height > 2.0f ) - { - m_sBannerFile = arrayImages[i]; - continue; - } - - /* Agh. DWI's inline title images are triggering this, resulting in - * kanji, etc., being used as a CDTitle for songs with none. Some - * sample data from random incarnations: - * 42x50 35x50 50x50 144x49 - * It looks like ~50 height is what people use to align to DWI's font. - * - * My tallest CDTitle is 44. Let's cut off in the middle and hope for - * the best. -(who? -aj) */ - /* The proper size of a CDTitle is 64x48 or sometihng. Simfile artists - * typically don't give a shit about this (see Cetaka's fucking banner - * -sized CDTitle). This is also subverted in certain designs (beta - * Mungyodance 3 simfiles, for instance, used the CDTitle to hold - * various information about the song in question). As it stands, - * I'm keeping this code until I figure out wtf to do -aj - */ - if( !HasCDTitle() && width<=100 && height<=48 ) - { - m_sCDTitleFile = arrayImages[i]; - continue; - } - - // Jacket files typically have the same width and height. - if( !HasJacket() && width == height ) - { - m_sJacketFile = arrayImages[i]; - continue; - } - - // Disc images are typically rectangular; make sure we have a banner already. - if( !HasDisc() && (width > height) && HasBanner() ) - { - if( arrayImages[i] != m_sBannerFile ) - m_sDiscFile = arrayImages[i]; - continue; - } - - // CD images are the same as Jackets, typically the same width and height - if( !HasCDImage() && width == height ) - { - m_sCDFile = arrayImages[i]; - continue; - } - } - - } - - // These will be written to cache, for Song::LoadFromSongDir to use later. - m_bHasMusic = HasMusic(); - m_bHasBanner = HasBanner(); - m_bHasBackground = HasBackground(); - - if( HasBanner() ) - BANNERCACHE->CacheBanner( GetBannerPath() ); - /* - if( HasBackground() ) - BANNERCACHE->CacheBackground( GetBackgroundPath() ); - */ - - // If no BGChanges are specified and there are movies in the song directory, then assume - // they are DWI style where the movie begins at beat 0. - if( (!HasBGChanges() && !fromCache) ) - { - vector arrayPossibleMovies; - GetDirListing( m_sSongDir + RString("*.ogv"), arrayPossibleMovies ); - GetDirListing( m_sSongDir + RString("*.avi"), arrayPossibleMovies ); - GetDirListing( m_sSongDir + RString("*.mpg"), arrayPossibleMovies ); - GetDirListing( m_sSongDir + RString("*.mpeg"), arrayPossibleMovies ); - /* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the - * music starts. */ - if( arrayPossibleMovies.size() == 1 ) - this->AddBackgroundChange(BACKGROUND_LAYER_1, - BackgroundChange(0, - arrayPossibleMovies[0], - "", - 1.f, - SBE_StretchNoLoop)); - } - - - /* Don't allow multiple Steps of the same StepsType and Difficulty (except - * for edits). We should be able to use difficulty names as unique - * identifiers for steps. */ - if( !fromCache ) - { - SongUtil::AdjustDuplicateSteps( this ); - } -} - -void Song::TranslateTitles() -{ - static TitleSubst tsub("Songs"); - - TitleFields title; - title.LoadFromStrings(m_sMainTitle, m_sSubTitle, m_sArtist, - m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); - tsub.Subst( title ); - title.SaveToStrings(m_sMainTitle, m_sSubTitle, m_sArtist, - m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); -} - -void Song::ReCalculateRadarValuesAndLastSecond(bool fromCache, bool duringCache) -{ - if( fromCache && this->GetFirstSecond() >= 0 && this->GetLastSecond() > 0 ) - { - // this is loaded from cache, then we just have to calculate the radar values. - for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); - return; - } - - float localFirst = FLT_MAX; // inf - // Make sure we're at least as long as the specified amount below. - float localLast = this->specifiedLastSecond; - - for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); - - // Must initialize before the gotos. - NoteData tempNoteData; - pSteps->GetNoteData( tempNoteData ); - - // calculate lastSecond - - // If it's autogen, then first/last beat will come from the parent. - if( pSteps->IsAutogen() ) - goto wipe_notedata; - - /* Don't calculate with edits unless the song only contains an edit - * chart, like those in Mungyodance 3. Otherwise, edits installed on - * the machine could extend the length of the song. */ - if( pSteps->IsAnEdit() && m_vpSteps.size() > 1 ) - goto wipe_notedata; - - // Don't set first/last beat based on lights. They often start very - // early and end very late. - if( pSteps->m_StepsType == StepsType_lights_cabinet ) - continue; // no need to wipe this. - - /* Many songs have stray, empty song patterns. Ignore them, so they - * don't force the first beat of the whole song to 0. */ - if( tempNoteData.GetLastRow() != 0 ) - { - localFirst = min(localFirst, - pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetFirstBeat())); - localLast = max(localLast, - pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetLastBeat())); - } - wipe_notedata: - if (duringCache) - { - NoteData dummy; - dummy.SetNumTracks(tempNoteData.GetNumTracks()); - pSteps->SetNoteData(dummy); - } - } - - // Yes, for some reason we can have freaky stuff take place here. - this->firstSecond = (localFirst < localLast) ? localFirst : 0; - this->lastSecond = localLast; -} - -// Return whether the song is playable in the given style. -bool Song::SongCompleteForStyle( const Style *st ) const -{ - return HasStepsType( st->m_StepsType ); -} - -bool Song::HasStepsType( StepsType st ) const -{ - return SongUtil::GetOneSteps( this, st ) != NULL; -} - -bool Song::HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const -{ - return SongUtil::GetOneSteps( this, st, dc ) != NULL; -} - -void Song::Save() -{ - LOG->Trace( "Song::SaveToSongFile()" ); - - ReCalculateRadarValuesAndLastSecond(); - TranslateTitles(); - - // Save the new files. These calls make backups on their own. - if( !SaveToSSCFile(GetSongFilePath(), false) ) - return; - SaveToSMFile(); - //SaveToDWIFile(); - SaveToCacheFile(); - - /* We've safely written our files and created backups. Rename non-SM and - * non-DWI files to avoid confusion. */ - vector arrayOldFileNames; - GetDirListing( m_sSongDir + "*.bms", arrayOldFileNames ); - GetDirListing( m_sSongDir + "*.pms", arrayOldFileNames ); - GetDirListing( m_sSongDir + "*.ksf", arrayOldFileNames ); - - for( unsigned i=0; iUserLog( "Song file", sOldPath, "couldn't be backed up." ); - // Don't remove. - } - else - FILEMAN->Remove( sOldPath ); - } -} - -bool Song::SaveToSMFile() -{ - const RString sPath = SetExtension( GetSongFilePath(), "sm" ); - LOG->Trace( "Song::SaveToSMFile(%s)", sPath.c_str() ); - - // If the file exists, make a backup. - if( IsAFile(sPath) ) - FileCopy( sPath, sPath + ".old" ); - - vector vpStepsToSave; - FOREACH_CONST( Steps*, m_vpSteps, s ) - { - Steps *pSteps = *s; - if( pSteps->IsAutogen() ) - continue; // don't write autogen notes - - // Only save steps that weren't loaded from a profile. - if( pSteps->WasLoadedFromProfile() ) - continue; - - vpStepsToSave.push_back( pSteps ); - } - - return NotesWriterSM::Write( sPath, *this, vpStepsToSave ); - -} - -bool Song::SaveToSSCFile( RString sPath, bool bSavingCache ) -{ - RString path = sPath; - if (!bSavingCache) - path = SetExtension(sPath, "ssc"); - - LOG->Trace( "Song::SaveToSSCFile('%s')", path.c_str() ); - - // If the file exists, make a backup. - if( !bSavingCache && IsAFile(path) ) - FileCopy( path, path + ".old" ); - - vector vpStepsToSave; - FOREACH_CONST( Steps*, m_vpSteps, s ) - { - Steps *pSteps = *s; - if( pSteps->IsAutogen() ) - continue; // don't write autogen notes - - // Only save steps that weren't loaded from a profile. - if( pSteps->WasLoadedFromProfile() ) - continue; - - if (!bSavingCache) - pSteps->SetFilename(path); - vpStepsToSave.push_back( pSteps ); - } - - if (bSavingCache) - { - return NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache); - } - - if( !NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache) ) - return false; - - if( g_BackUpAllSongSaves.Get() ) - { - RString sExt = GetExtension( path ); - RString sBackupFile = SetExtension( path, "" ); - - time_t cur_time; - time( &cur_time ); - struct tm now; - localtime_r( &cur_time, &now ); - - sBackupFile += ssprintf( "-%04i-%02i-%02i--%02i-%02i-%02i", - 1900+now.tm_year, now.tm_mon+1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec ); - sBackupFile = SetExtension( sBackupFile, sExt ); - sBackupFile += ssprintf( ".old" ); - - if( FileCopy(path, sBackupFile) ) - LOG->Trace( "Backed up %s to %s", path.c_str(), sBackupFile.c_str() ); - else - LOG->Trace( "Failed to back up %s to %s", path.c_str(), sBackupFile.c_str() ); - } - - // Mark these steps saved to disk. - FOREACH( Steps*, vpStepsToSave, s ) - (*s)->SetSavedToDisk( true ); - - return true; -} - -bool Song::SaveToCacheFile() -{ - SONGINDEX->AddCacheIndex(m_sSongDir, GetHashForDirectory(m_sSongDir)); - const RString sPath = GetCacheFilePath(); - return SaveToSSCFile(sPath, true); -} - -bool Song::SaveToDWIFile() -{ - const RString sPath = SetExtension( GetSongFilePath(), "dwi" ); - LOG->Trace( "Song::SaveToDWIFile(%s)", sPath.c_str() ); - - // If the file exists, make a backup. - if( IsAFile(sPath) ) - FileCopy( sPath, sPath + ".old" ); - - return NotesWriterDWI::Write( sPath, *this ); -} - - -void Song::AddAutoGenNotes() -{ - bool HasNotes[NUM_StepsType]; - memset( HasNotes, 0, sizeof(HasNotes) ); - for( unsigned i=0; i < m_vpSteps.size(); i++ ) // foreach Steps - { - if( m_vpSteps[i]->IsAutogen() ) - continue; - - StepsType st = m_vpSteps[i]->m_StepsType; - HasNotes[st] = true; - } - - FOREACH_ENUM( StepsType, stMissing ) - { - if( HasNotes[stMissing] ) - continue; - - // If m_bAutogenSteps is disabled, only autogen lights. - if( !PREFSMAN->m_bAutogenSteps && stMissing != StepsType_lights_cabinet ) - continue; - if( !GAMEMAN->GetStepsTypeInfo(stMissing).bAllowAutogen ) - continue; - - // missing Steps of this type - int iNumTracksOfMissing = GAMEMAN->GetStepsTypeInfo(stMissing).iNumTracks; - - // look for closest match - StepsType stBestMatch = StepsType_Invalid; - int iBestTrackDifference = INT_MAX; - - FOREACH_ENUM( StepsType, st ) - { - if( !HasNotes[st] ) - continue; - - // has (non-autogen) Steps of this type - const int iNumTracks = GAMEMAN->GetStepsTypeInfo(st).iNumTracks; - const int iTrackDifference = abs(iNumTracks-iNumTracksOfMissing); - if( iTrackDifference < iBestTrackDifference ) - { - stBestMatch = st; - iBestTrackDifference = iTrackDifference; - } - } - - if( stBestMatch != StepsType_Invalid ) - AutoGen( stMissing, stBestMatch ); - } -} - -void Song::AutoGen( StepsType ntTo, StepsType ntFrom ) -{ - // int iNumTracksOfTo = GAMEMAN->StepsTypeToNumTracks(ntTo); - - for( unsigned int j=0; jm_StepsType == ntFrom ) - { - Steps* pNewNotes = new Steps; - pNewNotes->AutogenFrom( pOriginalNotes, ntTo ); - this->AddSteps( pNewNotes ); - } - } -} - -void Song::RemoveAutoGenNotes() -{ - FOREACH_ENUM( StepsType, st ) - { - for( int j=m_vpStepsByType[st].size()-1; j>=0; j-- ) - { - if( m_vpStepsByType[st][j]->IsAutogen() ) - { - // delete m_vpSteps[j]; // delete below - m_vpStepsByType[st].erase( m_vpStepsByType[st].begin()+j ); - } - } - } - - for( int j=m_vpSteps.size()-1; j>=0; j-- ) - { - if( m_vpSteps[j]->IsAutogen() ) - { - delete m_vpSteps[j]; - m_vpSteps.erase( m_vpSteps.begin()+j ); - } - } -} - -bool Song::IsEasy( StepsType st ) const -{ - /* Very fast songs and songs with wide tempo changes are hard for new - * players, even if they have beginner steps. */ - DisplayBpms bpms; - this->GetDisplayBpms(bpms); - if( bpms.GetMax() >= 250 || bpms.GetMax() - bpms.GetMin() >= 75 ) - return false; - - /* The easy marker indicates which songs a beginner, having selected - * "beginner", can play and actually get a very easy song: if there are - * actual beginner steps, or if the light steps are 1- or 2-foot. */ - const Steps* pBeginnerNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Beginner ); - if( pBeginnerNotes ) - return true; - - const Steps* pEasyNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Easy ); - if( pEasyNotes && pEasyNotes->GetMeter() == 1 ) - return true; - - return false; -} - -bool Song::IsTutorial() const -{ - // A Song is considered a Tutorial if it has only Beginner steps. - FOREACH_CONST( Steps*, m_vpSteps, s ) - { - if( (*s)->m_StepsType == StepsType_lights_cabinet ) - continue; // ignore - if( (*s)->GetDifficulty() != Difficulty_Beginner ) - return false; - } - - return true; -} - -bool Song::HasEdits( StepsType st ) const -{ - for( unsigned i=0; im_StepsType == st && - pSteps->GetDifficulty() == Difficulty_Edit ) - { - return true; - } - } - - return false; -} - -bool Song::NormallyDisplayed() const -{ - return UNLOCKMAN == NULL || !UNLOCKMAN->SongIsLocked(this); -} - -bool Song::ShowInDemonstrationAndRanking() const { return !IsTutorial() && NormallyDisplayed(); } - - -// Hack: see Song::TidyUpData comments. -bool Song::HasMusic() const -{ - // If we have keys, we always have music. - if( m_vsKeysoundFile.size() != 0 ) - return true; - - return m_sMusicFile != "" && IsAFile(GetMusicPath()); -} -bool Song::HasBanner() const -{ - return m_sBannerFile != "" && IsAFile(GetBannerPath()); -} -bool Song::HasInstrumentTrack( InstrumentTrack it ) const -{ - return m_sInstrumentTrackFile[it] != "" && IsAFile(GetInstrumentTrackPath(it)); -} -bool Song::HasLyrics() const -{ - return m_sLyricsFile != "" && IsAFile(GetLyricsPath()); -} -bool Song::HasBackground() const -{ - return m_sBackgroundFile != "" && IsAFile(GetBackgroundPath()); -} -bool Song::HasCDTitle() const -{ - return m_sCDTitleFile != "" && IsAFile(GetCDTitlePath()); -} -bool Song::HasBGChanges() const -{ - FOREACH_BackgroundLayer( i ) - { - if( !GetBackgroundChanges(i).empty() ) - return true; - } - return false; -} -bool Song::HasAttacks() const {return !m_Attacks.empty(); } -bool Song::HasJacket() const -{ - return m_sJacketFile != "" && IsAFile(GetJacketPath()); -} -bool Song::HasDisc() const -{ - return m_sDiscFile != "" && IsAFile(GetDiscPath()); -} -bool Song::HasCDImage() const -{ - return m_sCDFile != "" && IsAFile(GetCDImagePath()); -} -bool Song::HasPreviewVid() const -{ - return m_sPreviewVidFile != "" && IsAFile(GetPreviewVidPath()); -} - -const vector &Song::GetBackgroundChanges( BackgroundLayer bl ) const -{ - return *(m_BackgroundChanges[bl]); -} -vector &Song::GetBackgroundChanges( BackgroundLayer bl ) -{ - return *(m_BackgroundChanges[bl].Get()); -} - -const vector &Song::GetForegroundChanges() const -{ - return *m_ForegroundChanges; -} -vector &Song::GetForegroundChanges() -{ - return *m_ForegroundChanges.Get(); -} - -vector Song::GetChangesToVectorString(const vector & changes) const -{ - vector ret; - FOREACH_CONST( BackgroundChange, changes, bgc ) - { - ret.push_back((*bgc).ToString()); - } - return ret; -} - -vector Song::GetBGChanges1ToVectorString() const -{ - return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_1)); -} - -vector Song::GetBGChanges2ToVectorString() const -{ - return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_2)); -} - -vector Song::GetFGChanges1ToVectorString() const -{ - return this->GetChangesToVectorString(this->GetForegroundChanges()); -} - -vector Song::GetInstrumentTracksToVectorString() const -{ - vector ret; - FOREACH_ENUM(InstrumentTrack, it) - { - if (this->HasInstrumentTrack(it)) - { - ret.push_back(InstrumentTrackToString(it) - + "=" - + this->m_sInstrumentTrackFile[it]); - } - } - return ret; -} - -RString GetSongAssetPath( RString sPath, const RString &sSongPath ) -{ - if( sPath == "" ) - return RString(); - - /* If there's no path in the file, the file is in the same directory as the - * song. (This is the preferred configuration.) */ - if( sPath.find('/') == string::npos ) - return sSongPath+sPath; - - // The song contains a path; treat it as relative to the top SM directory. - if( sPath.Left(3) == "../" ) - { - // The path begins with "../". Resolve it wrt. the song directory. - sPath = sSongPath + sPath; - } - - CollapsePath( sPath ); - - /* If the path still begins with "../", then there were an unreasonable number - * of them at the beginning of the path. Ignore the path entirely. */ - if( sPath.Left(3) == "../" ) - return RString(); - - return sPath; -} - -/* Note that supplying a path relative to the top-level directory is only for - * compatibility with DWI. We prefer paths relative to the song directory. */ -RString Song::GetMusicPath() const -{ - return GetSongAssetPath( m_sMusicFile, m_sSongDir ); -} - -RString Song::GetInstrumentTrackPath( InstrumentTrack it ) const -{ - return GetSongAssetPath( m_sInstrumentTrackFile[it], m_sSongDir ); -} - -RString Song::GetBannerPath() const -{ - return GetSongAssetPath( m_sBannerFile, m_sSongDir ); -} - -RString Song::GetLyricsPath() const -{ - return GetSongAssetPath( m_sLyricsFile, m_sSongDir ); -} - -RString Song::GetCDTitlePath() const -{ - return GetSongAssetPath( m_sCDTitleFile, m_sSongDir ); -} - -RString Song::GetBackgroundPath() const -{ - return GetSongAssetPath( m_sBackgroundFile, m_sSongDir ); -} - -RString Song::GetJacketPath() const -{ - return GetSongAssetPath( m_sJacketFile, m_sSongDir ); -} - -RString Song::GetDiscPath() const -{ - return GetSongAssetPath( m_sDiscFile, m_sSongDir ); -} - -RString Song::GetCDImagePath() const -{ - return GetSongAssetPath( m_sCDFile, m_sSongDir ); -} - -RString Song::GetPreviewVidPath() const -{ - return GetSongAssetPath( m_sPreviewVidFile, m_sSongDir ); -} - -RString Song::GetDisplayMainTitle() const -{ - if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitMainTitle(); - return m_sMainTitle; -} - -RString Song::GetDisplaySubTitle() const -{ - if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitSubTitle(); - return m_sSubTitle; -} - -RString Song::GetDisplayArtist() const -{ - if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitArtist(); - return m_sArtist; -} - -RString Song::GetDisplayFullTitle() const -{ - RString Title = GetDisplayMainTitle(); - RString SubTitle = GetDisplaySubTitle(); - - if(!SubTitle.empty()) Title += " " + SubTitle; - return Title; -} - -RString Song::GetTranslitFullTitle() const -{ - RString Title = GetTranslitMainTitle(); - RString SubTitle = GetTranslitSubTitle(); - - if(!SubTitle.empty()) Title += " " + SubTitle; - return Title; -} - -void Song::AddSteps( Steps* pSteps ) -{ - m_vpSteps.push_back( pSteps ); - ASSERT_M( pSteps->m_StepsType < NUM_StepsType, ssprintf("%i", pSteps->m_StepsType) ); - m_vpStepsByType[pSteps->m_StepsType].push_back( pSteps ); -} - -void Song::DeleteSteps( const Steps* pSteps, bool bReAutoGen ) -{ - ASSERT( !pSteps->IsAutogen() ); - - // Avoid any stale Note::parent pointers by removing all AutoGen'd Steps, - // then adding them again. - - if( bReAutoGen ) - RemoveAutoGenNotes(); - - vector &vpSteps = m_vpStepsByType[pSteps->m_StepsType]; - for( int j=vpSteps.size()-1; j>=0; j-- ) - { - if( vpSteps[j] == pSteps ) - { - //delete vpSteps[j]; // delete below - vpSteps.erase( vpSteps.begin()+j ); - break; - } - } - - for( int j=m_vpSteps.size()-1; j>=0; j-- ) - { - if( m_vpSteps[j] == pSteps ) - { - delete m_vpSteps[j]; - m_vpSteps.erase( m_vpSteps.begin()+j ); - break; - } - } - - if( bReAutoGen ) - AddAutoGenNotes(); -} - -bool Song::Matches(RString sGroup, RString sSong) const -{ - if( sGroup.size() && sGroup.CompareNoCase(this->m_sGroupName) != 0) - return false; - - RString sDir = this->GetSongDir(); - sDir.Replace("\\","/"); - vector bits; - split( sDir, "/", bits ); - ASSERT(bits.size() >= 2); // should always have at least two parts - const RString &sLastBit = bits[bits.size()-1]; - - // match on song dir or title (ala DWI) - if( !sSong.CompareNoCase(sLastBit) ) - return true; - if( !sSong.CompareNoCase(this->GetTranslitFullTitle()) ) - return true; - - return false; -} - -/* If apInUse is set, it contains a list of steps which are in use elsewhere, - * and should not be deleted. */ -void Song::FreeAllLoadedFromProfile( ProfileSlot slot, const set *setInUse ) -{ - /* DeleteSteps will remove and recreate autogen notes, which may reorder - * m_vpSteps, so be careful not to skip over entries. */ - vector apToRemove; - for( int s=m_vpSteps.size()-1; s>=0; s-- ) - { - Steps* pSteps = m_vpSteps[s]; - if( !pSteps->WasLoadedFromProfile() ) - continue; - if( slot != ProfileSlot_Invalid && pSteps->GetLoadedFromProfileSlot() != slot ) - continue; - if( setInUse != NULL && setInUse->find(pSteps) != setInUse->end() ) - continue; - apToRemove.push_back( pSteps ); - } - - for( unsigned i = 0; i < apToRemove.size(); ++i ) - this->DeleteSteps( apToRemove[i] ); -} - -void Song::GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const -{ - for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) - vpStepsOut.push_back( pSteps ); - } -} - -int Song::GetNumStepsLoadedFromProfile( ProfileSlot slot ) const -{ - int iCount = 0; - for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) - iCount++; - } - return iCount; -} - -bool Song::IsEditAlreadyLoaded( Steps* pSteps ) const -{ - ASSERT_M( pSteps->GetDifficulty() == Difficulty_Edit, - ssprintf("The %s chart for %s is no edit, thus it can't be checked for loading.", - DifficultyToString(pSteps->GetDifficulty()).c_str(), - this->m_sMainTitle.c_str())); - - for( unsigned i=0; iGetDifficulty() == Difficulty_Edit && - pOther->m_StepsType == pSteps->m_StepsType && - pOther->GetHash() == pSteps->GetHash() ) - { - return true; - } - } - - return false; -} - -bool Song::IsStepsUsingDifferentTiming(Steps *pSteps) const -{ - return pSteps->m_Timing != this->m_SongTiming; -} - -bool Song::HasSignificantBpmChangesOrStops() const -{ - if( m_SongTiming.HasStops() || m_SongTiming.HasDelays() ) - return true; - - // Don't consider BPM changes that only are only for maintaining sync as - // a real BpmChange. - if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) - { - if( m_fSpecifiedBPMMin != m_fSpecifiedBPMMax ) - return true; - } - else if( m_SongTiming.HasBpmChanges() ) - { - return true; - } - - return false; -} - -float Song::GetStepsSeconds() const -{ - return this->GetLastSecond() - this->GetFirstSecond(); -} - -bool Song::IsLong() const -{ - return !IsMarathon() && m_fMusicLengthSeconds >= g_fLongVerSongSeconds; -} - -bool Song::IsMarathon() const -{ - return m_fMusicLengthSeconds >= g_fMarathonVerSongSeconds; -} - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the Song. */ -class LunaSong: public Luna -{ -public: - static int GetDisplayFullTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplayFullTitle() ); return 1; - } - static int GetTranslitFullTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitFullTitle() ); return 1; - } - static int GetDisplayMainTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplayMainTitle() ); return 1; - } - static int GetTranslitMainTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitMainTitle() ); return 1; - } - static int GetDisplaySubTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplaySubTitle() ); return 1; - } - static int GetTranslitSubTitle( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitSubTitle() ); return 1; - } - static int GetDisplayArtist( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetDisplayArtist() ); return 1; - } - static int GetTranslitArtist( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetTranslitArtist() ); return 1; - } - static int GetGenre( T* p, lua_State *L ) - { - lua_pushstring(L, p->m_sGenre ); return 1; - } - static int GetOrigin( T* p, lua_State *L ) - { - lua_pushstring(L, p->m_sOrigin ); return 1; - } - static int GetAllSteps( T* p, lua_State *L ) - { - const vector &v = p->GetAllSteps(); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - static int GetStepsByStepsType( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - const vector &v = p->GetStepsByStepsType( st ); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - static int GetSongDir( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetSongDir() ); - return 1; - } - static int GetMusicPath( T* p, lua_State *L ) - { - RString s = p->GetMusicPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetBannerPath( T* p, lua_State *L ) - { - RString s = p->GetBannerPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetBackgroundPath( T* p, lua_State *L ) - { - RString s = p->GetBackgroundPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetPreviewVidPath( T* p, lua_State *L ) - { - RString s = p->GetPreviewVidPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetJacketPath( T* p, lua_State *L ) - { - RString s = p->GetJacketPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetCDImagePath( T* p, lua_State *L ) - { - RString s = p->GetCDImagePath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetDiscPath( T* p, lua_State *L ) - { - RString s = p->GetDiscPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetCDTitlePath( T* p, lua_State *L ) - { - RString s = p->GetCDTitlePath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetLyricsPath( T* p, lua_State *L ) - { - RString s = p->GetLyricsPath(); - if( !s.empty() ) - lua_pushstring(L, s); - else - lua_pushnil(L); - return 1; - } - static int GetSongFilePath( T* p, lua_State *L ) - { - lua_pushstring(L, p->GetSongFilePath() ); - return 1; - } - static int IsTutorial( T* p, lua_State *L ) - { - lua_pushboolean(L, p->IsTutorial()); - return 1; - } - static int IsEnabled( T* p, lua_State *L ) - { - lua_pushboolean(L, p->GetEnabled()); - return 1; - } - static int GetGroupName( T* p, lua_State *L ) - { - lua_pushstring(L, p->m_sGroupName); - return 1; - } - static int MusicLengthSeconds( T* p, lua_State *L ) - { - lua_pushnumber(L, p->m_fMusicLengthSeconds); - return 1; - } - static int GetSampleStart( T* p, lua_State *L ) - { - lua_pushnumber(L, p->m_fMusicSampleStartSeconds); - return 1; - } - static int GetSampleLength( T* p, lua_State *L ) - { - lua_pushnumber(L, p->m_fMusicSampleLengthSeconds); - return 1; - } - static int IsLong( T* p, lua_State *L ) - { - lua_pushboolean(L, p->IsLong()); - return 1; - } - static int IsMarathon( T* p, lua_State *L ) - { - lua_pushboolean(L, p->IsMarathon()); - return 1; - } - static int HasStepsType( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - lua_pushboolean( L, p->HasStepsType(st) ); - return 1; - } - static int HasStepsTypeAndDifficulty( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - Difficulty dc = Enum::Check( L, 2 ); - lua_pushboolean( L, p->HasStepsTypeAndDifficulty(st, dc) ); - return 1; - } - static int IsStepsUsingDifferentTiming(T* p, lua_State *L) - { - lua_pushboolean(L, p->IsStepsUsingDifferentTiming(Luna::check( L, 1, true ))); - return 1; - } - /* TODO: HasStepsTypeAndDifficulty and GetOneSteps should be in - * a SongUtil Lua table and a method of Steps. */ - static int GetOneSteps( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - Difficulty dc = Enum::Check( L, 2 ); - Steps *pSteps = SongUtil::GetOneSteps( p, st, dc ); - if( pSteps ) - pSteps->PushSelf(L); - else - lua_pushnil(L); - return 1; - } - static int GetTimingData( T* p, lua_State *L ) { p->m_SongTiming.PushSelf(L); return 1; } - // has functions - static int HasMusic( T* p, lua_State *L ) { lua_pushboolean(L, p->HasMusic()); return 1; } - static int HasBanner( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBanner()); return 1; } - static int HasBackground( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBackground()); return 1; } - static int HasPreviewVid( T* p, lua_State *L ) { lua_pushboolean(L, p->HasPreviewVid()); return 1; } - static int HasJacket( T* p, lua_State *L ) { lua_pushboolean(L, p->HasJacket()); return 1; } - static int HasDisc( T* p, lua_State *L ) { lua_pushboolean(L, p->HasDisc()); return 1; } - static int HasCDImage( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDImage()); return 1; } - static int HasCDTitle( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDTitle()); return 1; } - static int HasBGChanges( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBGChanges()); return 1; } - static int HasLyrics( T* p, lua_State *L ) { lua_pushboolean(L, p->HasLyrics()); return 1; } - // functions that AJ loves - static int HasSignificantBPMChangesOrStops( T* p, lua_State *L ) - { - lua_pushboolean(L, p->HasSignificantBpmChangesOrStops()); - return 1; - } - static int HasEdits( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - lua_pushboolean(L, p->HasEdits( st )); - return 1; - } - static int IsEasy( T* p, lua_State *L ) - { - StepsType st = Enum::Check(L, 1); - lua_pushboolean(L, p->IsEasy( st )); - return 1; - } - static int GetStepsSeconds( T* p, lua_State *L ) - { - lua_pushnumber(L, p->GetStepsSeconds()); - return 1; - } - static int NormallyDisplayed( T* p, lua_State *L ) - { - lua_pushboolean(L, p->NormallyDisplayed()); - return 1; - } - static int ShowInDemonstrationAndRanking( T* p, lua_State *L ) - { - lua_pushboolean(L, p->ShowInDemonstrationAndRanking()); - return 1; - } - static int GetFirstSecond(T* p, lua_State *L) - { - lua_pushnumber(L, p->GetFirstSecond()); - return 1; - } - static int GetLastSecond(T* p, lua_State *L) - { - lua_pushnumber(L, p->GetLastSecond()); - return 1; - } - static int GetFirstBeat( T* p, lua_State *L ) - { - lua_pushnumber(L, p->GetFirstBeat()); - return 1; - } - static int GetLastBeat( T* p, lua_State *L ) - { - lua_pushnumber(L, p->GetLastBeat()); - return 1; - } - static int HasAttacks( T* p, lua_State *L ) - { - lua_pushboolean(L, p->HasAttacks()); - return 1; - } - static int GetDisplayBpms( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - float fMin = temp.GetMin(); - float fMax = temp.GetMax(); - vector fBPMs; - fBPMs.push_back( fMin ); - fBPMs.push_back( fMax ); - LuaHelpers::CreateTableFromArray(fBPMs, L); - return 1; - } - static int IsDisplayBpmSecret( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - lua_pushboolean( L, temp.IsSecret() ); - return 1; - } - static int IsDisplayBpmConstant( T* p, lua_State *L ) - { - DisplayBpms temp; - p->GetDisplayBpms(temp); - lua_pushboolean( L, temp.BpmIsConstant() ); - return 1; - } - static int IsDisplayBpmRandom( T* p, lua_State *L ) - { - lua_pushboolean( L, p->m_DisplayBPMType == DISPLAY_BPM_RANDOM ); - return 1; - } - - LunaSong() - { - ADD_METHOD( GetDisplayFullTitle ); - ADD_METHOD( GetTranslitFullTitle ); - ADD_METHOD( GetDisplayMainTitle ); - ADD_METHOD( GetTranslitMainTitle ); - ADD_METHOD( GetDisplaySubTitle ); - ADD_METHOD( GetTranslitSubTitle ); - ADD_METHOD( GetDisplayArtist ); - ADD_METHOD( GetTranslitArtist ); - ADD_METHOD( GetGenre ); - ADD_METHOD( GetOrigin ); - ADD_METHOD( GetAllSteps ); - ADD_METHOD( GetStepsByStepsType ); - ADD_METHOD( GetSongDir ); - ADD_METHOD( GetMusicPath ); - ADD_METHOD( GetBannerPath ); - ADD_METHOD( GetBackgroundPath ); - ADD_METHOD( GetJacketPath ); - ADD_METHOD( GetCDImagePath ); - ADD_METHOD( GetDiscPath ); - ADD_METHOD( GetCDTitlePath ); - ADD_METHOD( GetLyricsPath ); - ADD_METHOD( GetSongFilePath ); - ADD_METHOD( IsTutorial ); - ADD_METHOD( IsEnabled ); - ADD_METHOD( GetGroupName ); - ADD_METHOD( MusicLengthSeconds ); - ADD_METHOD( GetSampleStart ); - ADD_METHOD( GetSampleLength ); - ADD_METHOD( IsLong ); - ADD_METHOD( IsMarathon ); - ADD_METHOD( HasStepsType ); - ADD_METHOD( HasStepsTypeAndDifficulty ); - ADD_METHOD( GetOneSteps ); - ADD_METHOD( GetTimingData ); - ADD_METHOD( HasMusic ); - ADD_METHOD( HasBanner ); - ADD_METHOD( HasBackground ); - ADD_METHOD( HasJacket ); - ADD_METHOD( HasCDImage ); - ADD_METHOD( HasDisc ); - ADD_METHOD( HasCDTitle ); - ADD_METHOD( HasBGChanges ); - ADD_METHOD( HasLyrics ); - ADD_METHOD( HasSignificantBPMChangesOrStops ); - ADD_METHOD( HasEdits ); - ADD_METHOD( IsEasy ); - ADD_METHOD( GetStepsSeconds ); - ADD_METHOD( NormallyDisplayed ); - ADD_METHOD( GetFirstBeat ); - ADD_METHOD( GetFirstSecond ); - ADD_METHOD( GetLastBeat ); - ADD_METHOD( GetLastSecond ); - ADD_METHOD( HasAttacks ); - ADD_METHOD( GetDisplayBpms ); - ADD_METHOD( IsDisplayBpmSecret ); - ADD_METHOD( IsDisplayBpmConstant ); - ADD_METHOD( IsDisplayBpmRandom ); - ADD_METHOD( IsStepsUsingDifferentTiming ); - ADD_METHOD( ShowInDemonstrationAndRanking ); - ADD_METHOD( HasPreviewVid ); - ADD_METHOD( GetPreviewVidPath ); - } -}; - -LUA_REGISTER_CLASS( Song ) -// lua end - - -/* - * (c) 2001-2004 Chris Danford, Glenn Maynard - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#include "global.h" +#include "Song.h" +#include "Steps.h" +#include "RageUtil.h" +#include "RageLog.h" +#include "NoteData.h" +#include "RageSoundReader_FileReader.h" +#include "RageSurface_Load.h" +#include "SongCacheIndex.h" +#include "GameManager.h" +#include "PrefsManager.h" +#include "Style.h" +#include "FontCharAliases.h" +#include "TitleSubstitution.h" +#include "BannerCache.h" +//#include "BackgroundCache.h" +#include "Sprite.h" +#include "RageFileManager.h" +#include "RageSurface.h" +#include "NoteDataUtil.h" +#include "SongUtil.h" +#include "StepsUtil.h" +#include "Foreach.h" +#include "BackgroundUtil.h" +#include "SpecialFiles.h" +#include "NotesLoader.h" +#include "NotesLoaderSM.h" +#include "NotesLoaderSSC.h" +#include "NotesWriterDWI.h" +#include "NotesWriterJson.h" +#include "NotesWriterSM.h" +#include "NotesWriterSSC.h" +#include "UnlockManager.h" +#include "LyricsLoader.h" + +#include +#include +#include + + +/** + * @brief The internal version of the cache for StepMania. + * + * Increment this value to invalidate the current cache. */ +const int FILE_CACHE_VERSION = 208; + +/** @brief How long does a song sample last by default? */ +const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f; + +static Preference g_fLongVerSongSeconds( "LongVerSongSeconds", 60*2.5f ); +static Preference g_fMarathonVerSongSeconds( "MarathonVerSongSeconds", 60*5.f ); +static Preference g_BackUpAllSongSaves( "BackUpAllSongSaves", false ); + +static const char *InstrumentTrackNames[] = { + "Guitar", + "Rhythm", + "Bass", +}; +XToString( InstrumentTrack ); +StringToX( InstrumentTrack ); + +Song::Song() +{ + FOREACH_BackgroundLayer( i ) + m_BackgroundChanges[i] = AutoPtrCopyOnWrite(new VBackgroundChange); + m_ForegroundChanges = AutoPtrCopyOnWrite(new VBackgroundChange); + + m_LoadedFromProfile = ProfileSlot_Invalid; + m_fVersion = STEPFILE_VERSION_NUMBER; + m_fMusicSampleStartSeconds = -1; + m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; + m_fMusicLengthSeconds = 0; + firstSecond = -1; + lastSecond = -1; + specifiedLastSecond = -1; + m_SelectionDisplay = SHOW_ALWAYS; + m_bEnabled = true; + m_DisplayBPMType = DISPLAY_BPM_ACTUAL; + m_fSpecifiedBPMMin = 0; + m_fSpecifiedBPMMax = 0; + m_bIsSymLink = false; + m_bHasMusic = false; + m_bHasBanner = false; + m_bHasBackground = false; +} + +Song::~Song() +{ + FOREACH( Steps*, m_vpSteps, s ) + SAFE_DELETE( *s ); + m_vpSteps.clear(); + + // It's the responsibility of the owner of this Song to make sure + // that all pointers to this Song and its Steps are invalidated. +} + +void Song::DetachSteps() +{ + m_vpSteps.clear(); + FOREACH_ENUM( StepsType, st ) + m_vpStepsByType[st].clear(); +} + +float Song::GetFirstSecond() const +{ + return this->firstSecond; +} + +float Song::GetFirstBeat() const +{ + return this->m_SongTiming.GetBeatFromElapsedTime(this->firstSecond); +} + +float Song::GetLastSecond() const +{ + return this->lastSecond; +} + +float Song::GetLastBeat() const +{ + return this->m_SongTiming.GetBeatFromElapsedTime(this->lastSecond); +} + +float Song::GetSpecifiedLastSecond() const +{ + return this->specifiedLastSecond; +} + +float Song::GetSpecifiedLastBeat() const +{ + return this->m_SongTiming.GetBeatFromElapsedTime(this->specifiedLastSecond); +} + +void Song::SetFirstSecond(const float f) +{ + this->firstSecond = f; +} + +void Song::SetLastSecond(const float f) +{ + this->lastSecond = f; +} + +void Song::SetSpecifiedLastSecond(const float f) +{ + this->specifiedLastSecond = f; +} + +// Reset to an empty song. +void Song::Reset() +{ + FOREACH( Steps*, m_vpSteps, s ) + SAFE_DELETE( *s ); + m_vpSteps.clear(); + FOREACH_ENUM( StepsType, st ) + m_vpStepsByType[st].clear(); + + Song empty; + *this = empty; + + // It's the responsibility of the owner of this Song to make sure + // that all pointers to this Song and its Steps are invalidated. +} + + +void Song::AddBackgroundChange( BackgroundLayer iLayer, BackgroundChange seg ) +{ + // Delete old background change at this start beat, if any. + FOREACH( BackgroundChange, GetBackgroundChanges(iLayer), bgc ) + { + if( bgc->m_fStartBeat == seg.m_fStartBeat ) + { + GetBackgroundChanges(iLayer).erase( bgc ); + break; + } + } + + ASSERT( iLayer >= 0 && iLayer < NUM_BackgroundLayer ); + BackgroundUtil::AddBackgroundChange( GetBackgroundChanges(iLayer), seg ); +} + +void Song::AddForegroundChange( BackgroundChange seg ) +{ + BackgroundUtil::AddBackgroundChange( GetForegroundChanges(), seg ); +} + +void Song::AddLyricSegment( LyricSegment seg ) +{ + m_LyricSegments.push_back( seg ); +} + +Steps *Song::CreateSteps() +{ + Steps *pSteps = new Steps; + InitSteps( pSteps ); + return pSteps; +} + +void Song::InitSteps(Steps *pSteps) +{ + pSteps->m_Timing = this->m_SongTiming; + pSteps->m_sAttackString = this->m_sAttackString; + pSteps->m_Attacks = this->m_Attacks; + pSteps->SetDisplayBPM(this->m_DisplayBPMType); + pSteps->SetMinBPM(this->m_fSpecifiedBPMMin); + pSteps->SetMaxBPM(this->m_fSpecifiedBPMMax); +} + +void Song::GetDisplayBpms( DisplayBpms &AddTo ) const +{ + if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) + { + AddTo.Add( m_fSpecifiedBPMMin ); + AddTo.Add( m_fSpecifiedBPMMax ); + } + else + { + float fMinBPM, fMaxBPM; + m_SongTiming.GetActualBPM( fMinBPM, fMaxBPM ); + AddTo.Add( fMinBPM ); + AddTo.Add( fMaxBPM ); + } +} + +const BackgroundChange &Song::GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const +{ + for( unsigned i=0; i fBeat ) + return GetBackgroundChanges(iLayer)[i]; + return GetBackgroundChanges(iLayer)[0]; +} + + +RString Song::GetCacheFilePath() const +{ + return SongCacheIndex::GetCacheFilePath( "Songs", m_sSongDir ); +} + +// Get a path to the SM containing data for this song. It might be a cache file. +const RString &Song::GetSongFilePath() const +{ + ASSERT_M( !m_sSongFileName.empty(), + ssprintf("The song %s has no filename associated with it!", + this->m_sMainTitle.c_str())); + return m_sSongFileName; +} + +/* Hack: This should be a parameter to TidyUpData, but I don't want to pull in + * into Song.h, which is heavily used. */ +static set BlacklistedImages; + +/* If PREFSMAN->m_bFastLoad is true, always load from cache if possible. + * Don't read the contents of sDir if we can avoid it. That means we can't call + * HasMusic(), HasBanner() or GetHashForDirectory(). + * If true, check the directory hash and reload the song from scratch if it's changed. + */ +bool Song::LoadFromSongDir( RString sDir ) +{ +// LOG->Trace( "Song::LoadFromSongDir(%s)", sDir.c_str() ); + ASSERT_M( sDir != "", "Songs can't be loaded from an empty directory!" ); + + // make sure there is a trailing slash at the end of sDir + if( sDir.Right(1) != "/" ) + sDir += "/"; + + // save song dir + m_sSongDir = sDir; + + // save group name + vector sDirectoryParts; + split( m_sSongDir, "/", sDirectoryParts, false ); + ASSERT( sDirectoryParts.size() >= 4 ); /* e.g. "/Songs/Slow/Taps/" */ + m_sGroupName = sDirectoryParts[sDirectoryParts.size()-3]; // second from last item + ASSERT( m_sGroupName != "" ); + + // First, look in the cache for this song (without loading NoteData) + unsigned uCacheHash = SONGINDEX->GetCacheHash(m_sSongDir); + bool bUseCache = true; + RString sCacheFilePath = GetCacheFilePath(); + + if( !DoesFileExist(sCacheFilePath) ) + bUseCache = false; + if( !PREFSMAN->m_bFastLoad && GetHashForDirectory(m_sSongDir) != uCacheHash ) + bUseCache = false; // this cache is out of date + + if( bUseCache ) + { + /* + LOG->Trace("Loading '%s' from cache file '%s'.", + m_sSongDir.c_str(), + GetCacheFilePath().c_str()); + */ + SSCLoader loaderSSC; + bool bLoadedFromSSC = loaderSSC.LoadFromSimfile( sCacheFilePath, *this, true ); + if( !bLoadedFromSSC ) + { + // load from .sm + SMLoader loaderSM; + loaderSM.LoadFromSimfile( sCacheFilePath, *this, true ); + loaderSM.TidyUpData( *this, true ); + } + } + else + { + // There was no entry in the cache for this song, or it was out of date. + // Let's load it from a file, then write a cache entry. + + if( !NotesLoader::LoadFromDir(sDir, *this, BlacklistedImages) ) + { + LOG->UserLog( "Song", sDir, "has no SSC, SM, SMA, DWI, BMS, or KSF files." ); + + vector vs; + GetDirListing( sDir + "*.mp3", vs, false, false ); + GetDirListing( sDir + "*.oga", vs, false, false ); + GetDirListing( sDir + "*.ogg", vs, false, false ); + bool bHasMusic = !vs.empty(); + + if( !bHasMusic ) + { + LOG->UserLog( "Song", sDir, "has no music file either. Ignoring this song directory." ); + return false; + } + // Make sure we have a future filename figured out. + vector folders; + split(sDir, "/", folders); + RString songName = folders[2] + ".ssc"; + this->m_sSongFileName = sDir + songName; + // Continue on with a blank Song so that people can make adjustments using the editor. + } + TidyUpData(false, true); + + // save a cache file so we don't have to parse it all over again next time + if( !SaveToCacheFile() ) + sCacheFilePath = RString(); + } + + FOREACH( Steps*, m_vpSteps, s ) + { + /* Compress all Steps. During initial caching, this will remove cached + * NoteData; during cached loads, this will just remove cached SMData. */ + (*s)->Compress(); + } + + // Load the cached banners, if it's not loaded already. + if( PREFSMAN->m_BannerCache == BNCACHE_LOW_RES_PRELOAD && m_bHasBanner ) + BANNERCACHE->LoadBanner( GetBannerPath() ); + // Load the cached background, if it's not loaded already. + /* + if( PREFSMAN->m_BackgroundCache == BGCACHE_LOW_RES_PRELOAD && m_bHasBackground ) + BACKGROUNDCACHE->LoadBackground( GetBackgroundPath() ); + */ + + // Add AutoGen pointers. (These aren't cached.) + AddAutoGenNotes(); + + if( !m_bHasMusic ) + { + LOG->UserLog( "Song", sDir, "has no music; ignored." ); + return false; // don't load this song + } + + return true; // do load this song +} + +bool Song::ReloadFromSongDir( RString sDir ) +{ + RemoveAutoGenNotes(); + vector vOldSteps = m_vpSteps; + + Song copy; + if( !copy.LoadFromSongDir( sDir ) ) + return false; + copy.RemoveAutoGenNotes(); + *this = copy; + + // First we assemble a map to let us easily find the new steps + map mNewSteps; + for( vector::const_iterator it = m_vpSteps.begin(); it != m_vpSteps.end(); ++it ) + { + StepsID id; + id.FromSteps( *it ); + mNewSteps[id] = *it; + } + + // Now we wipe out the new pointers, which were shallow copied and not deep copied... + m_vpSteps.clear(); + FOREACH_ENUM( StepsType, i ) + m_vpStepsByType[i].clear(); + + /* Then we copy as many Steps as possible on top of the old pointers. + * The only pointers that change are pointers to Steps that are not in the + * reverted file, which we delete, and pointers to Steps that are in the + * reverted file but not the original *this, which we create new copies of. + * We have to go through these hoops because many places assume the Steps + * pointers don't change - even though there are other ways they can change, + * such as deleting a Steps via the editor. */ + for( vector::const_iterator itOld = vOldSteps.begin(); itOld != vOldSteps.end(); ++itOld ) + { + StepsID id; + id.FromSteps( *itOld ); + map::iterator itNew = mNewSteps.find( id ); + if( itNew == mNewSteps.end() ) + { + // This stepchart didn't exist in the file we reverted from + delete *itOld; + } + else + { + Steps *OldSteps = *itOld; + *OldSteps = *(itNew->second); + AddSteps( OldSteps ); + mNewSteps.erase( itNew ); + } + } + // The leftovers in the map are steps that didn't exist before we reverted + for( map::const_iterator it = mNewSteps.begin(); it != mNewSteps.end(); ++it ) + { + Steps *NewSteps = new Steps(); + *NewSteps = *(it->second); + AddSteps( NewSteps ); + } + + AddAutoGenNotes(); + return true; +} + +static void GetImageDirListing( RString sPath, vector &AddTo ) +{ + GetDirListing( sPath + ".png", AddTo, false, false ); + GetDirListing( sPath + ".jpg", AddTo, false, false ); + GetDirListing( sPath + ".jpeg", AddTo, false, false ); + GetDirListing( sPath + ".bmp", AddTo, false, false ); + GetDirListing( sPath + ".gif", AddTo, false, false ); +} + +/* Fix up song paths. If there's a leading "./", be sure to keep it: it's + * a signal that the path is from the root directory, not the song directory. + * Other than a leading "./", song paths must never contain "." or "..". */ +void FixupPath( RString &path, const RString &sSongPath ) +{ + // Replace backslashes with slashes in all paths. + FixSlashesInPlace( path ); + + /* Many imported files contain erroneous whitespace before or after + * filenames. Paths usually don't actually start or end with spaces, + * so let's just remove it. */ + Trim( path ); +} + +// Songs in BlacklistImages will never be autodetected as song images. +void Song::TidyUpData( bool fromCache, bool duringCache ) +{ + // We need to do this before calling any of HasMusic, HasHasCDTitle, etc. + ASSERT_M( m_sSongDir.Left(3) != "../", m_sSongDir ); // meaningless + FixupPath( m_sSongDir, "" ); + FixupPath( m_sMusicFile, m_sSongDir ); + FOREACH_ENUM( InstrumentTrack, i ) + if( !m_sInstrumentTrackFile[i].empty() ) + FixupPath( m_sInstrumentTrackFile[i], m_sSongDir ); + FixupPath( m_sBannerFile, m_sSongDir ); + FixupPath( m_sJacketFile, m_sSongDir ); + FixupPath( m_sCDFile, m_sSongDir ); + FixupPath( m_sDiscFile, m_sSongDir ); + FixupPath( m_sLyricsFile, m_sSongDir ); + FixupPath( m_sBackgroundFile, m_sSongDir ); + FixupPath( m_sCDTitleFile, m_sSongDir ); + + if( !HasMusic() ) + { + vector arrayPossibleMusic; + GetDirListing( m_sSongDir + RString("*.mp3"), arrayPossibleMusic ); + GetDirListing( m_sSongDir + RString("*.oga"), arrayPossibleMusic ); + GetDirListing( m_sSongDir + RString("*.ogg"), arrayPossibleMusic ); + GetDirListing( m_sSongDir + RString("*.wav"), arrayPossibleMusic ); + + if( !arrayPossibleMusic.empty() ) + { + int idx = 0; + /* If the first song is "intro", and we have more than one available, + * don't use it--it's probably a KSF intro music file, which we don't + * (yet) support. */ + if( arrayPossibleMusic.size() > 1 && + !arrayPossibleMusic[0].Left(5).CompareNoCase("intro") ) + ++idx; + + // we found a match + m_sMusicFile = arrayPossibleMusic[idx]; + } + } + + // This must be done before radar calculation. + if( HasMusic() ) + { + RString error; + RageSoundReader *Sample = RageSoundReader_FileReader::OpenFile( GetMusicPath(), error ); + /* XXX: Checking if the music file exists eliminates a warning + * originating from BMS files (which have no music file, per se) + * but it's something of a hack. */ + if( Sample == NULL && m_sMusicFile != "" ) + { + LOG->UserLog( "Sound file", GetMusicPath(), "couldn't be opened: %s", error.c_str() ); + + // Don't use this file. + m_sMusicFile = ""; + } + else if ( Sample != NULL ) + { + m_fMusicLengthSeconds = Sample->GetLength() / 1000.0f; + delete Sample; + + if( m_fMusicLengthSeconds < 0 ) + { + // It failed; bad file or something. It's already logged a warning. + m_fMusicLengthSeconds = 100; // guess + } + else if( m_fMusicLengthSeconds == 0 ) + { + LOG->UserLog( "Sound file", GetMusicPath(), "is empty." ); + } + } + } + else // ! HasMusic() + { + m_fMusicLengthSeconds = 100; // guess + LOG->UserLog("Song", + GetSongDir(), + "has no music file; guessing at %f seconds", + m_fMusicLengthSeconds); + } + + if( m_fMusicLengthSeconds < 0 ) + { + LOG->UserLog("Sound file", + GetMusicPath(), + "has a negative length %f.", + m_fMusicLengthSeconds); + m_fMusicLengthSeconds = 0; + } + + m_SongTiming.TidyUpData(); + + FOREACH( Steps *, m_vpSteps, s ) + { + (*s)->m_Timing.TidyUpData(); + } + + /* Generate these before we autogen notes, so the new notes can inherit + * their source's values. */ + ReCalculateRadarValuesAndLastSecond( fromCache, true ); + + Trim( m_sMainTitle ); + Trim( m_sSubTitle ); + Trim( m_sArtist ); + + // Fall back on the song directory name. + if( m_sMainTitle == "" ) + NotesLoader::GetMainAndSubTitlesFromFullTitle(Basename(this->GetSongDir()), + m_sMainTitle, m_sSubTitle ); + + if( m_sArtist == "" ) + m_sArtist = "Unknown artist"; + TranslateTitles(); + + if( m_fMusicSampleStartSeconds == -1 || + m_fMusicSampleLengthSeconds == 0 || + m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) + { + const TimingData &timing = this->m_SongTiming; + m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( 100 ); + + if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) + { + // Attempt to get a reasonable default. + int iBeat = lrintf(this->m_SongTiming.GetBeatFromElapsedTime(this->GetLastSecond())/2); + iBeat -= iBeat%4; + m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( (float)iBeat ); + } + } + + // The old logic meant that you couldn't have sample lengths that go forever, + // e.g. those in Donkey Konga. I never liked that. -freem + if( m_fMusicSampleLengthSeconds <= 0.00f ) + m_fMusicSampleLengthSeconds = DEFAULT_MUSIC_SAMPLE_LENGTH; + + // Here's the problem: We have a directory full of images. We want to + // determine which image is the banner, which is the background, and which + // is the CDTitle. + + CHECKPOINT_M( "Looking for images..." ); + + if( !fromCache ) + { + + // First, check the file name for hints. + if( !HasBanner() ) + { + /* If a nonexistant banner file is specified, and we can't find a + * replacement, don't wipe out the old value. */ + //m_sBannerFile = ""; + + // find an image with "banner" in the file name + vector arrayPossibleBanners; + GetImageDirListing( m_sSongDir + "*banner*", arrayPossibleBanners ); + + /* Some people do things differently for the sake of being different. + * Don't match eg. abnormal, numbness. */ + GetImageDirListing( m_sSongDir + "* BN", arrayPossibleBanners ); + + if( !arrayPossibleBanners.empty() ) + m_sBannerFile = arrayPossibleBanners[0]; + } + + if( !HasBackground() ) + { + //m_sBackgroundFile = ""; + + // find an image with "bg" or "background" in the file name + vector arrayPossibleBGs; + GetImageDirListing( m_sSongDir + "*background*", arrayPossibleBGs ); + // don't match e.g. "subgroup", "hobgoblin", etc. + GetImageDirListing( m_sSongDir + "*bg", arrayPossibleBGs ); + if( !arrayPossibleBGs.empty() ) + m_sBackgroundFile = arrayPossibleBGs[0]; + } + + if( !HasJacket() ) + { + // find an image with "jacket" or "albumart" in the filename. + vector arrayPossibleJackets; + GetImageDirListing( m_sSongDir + "jk_*", arrayPossibleJackets ); + GetImageDirListing( m_sSongDir + "*jacket*", arrayPossibleJackets ); + GetImageDirListing( m_sSongDir + "*albumart*", arrayPossibleJackets ); + if( !arrayPossibleJackets.empty() ) + m_sJacketFile = arrayPossibleJackets[0]; + } + + if( !HasCDImage() ) + { + // CD image, a la ddr 1st-3rd (not to be confused with CDTitles) + // find an image with "-cd" at the end of the filename. + vector arrayPossibleCDImages; + GetImageDirListing( m_sSongDir + "*-cd", arrayPossibleCDImages ); + if( !arrayPossibleCDImages.empty() ) + m_sCDFile = arrayPossibleCDImages[0]; + } + + if( !HasDisc() ) + { + // a rectangular graphic, not to be confused with CDImage above. + vector arrayPossibleDiscImages; + GetImageDirListing( m_sSongDir + "* disc", arrayPossibleDiscImages ); + GetImageDirListing( m_sSongDir + "* title", arrayPossibleDiscImages ); + if( !arrayPossibleDiscImages.empty() ) + m_sDiscFile = arrayPossibleDiscImages[0]; + } + + if( !HasCDTitle() ) + { + // find an image with "cdtitle" in the file name + vector arrayPossibleCDTitles; + GetImageDirListing( m_sSongDir + "*cdtitle*", arrayPossibleCDTitles ); + if( !arrayPossibleCDTitles.empty() ) + m_sCDTitleFile = arrayPossibleCDTitles[0]; + } + + if( !HasLyrics() ) + { + // Check if there is a lyric file in here + vector arrayLyricFiles; + GetDirListing(m_sSongDir + RString("*.lrc"), arrayLyricFiles ); + if( !arrayLyricFiles.empty() ) + m_sLyricsFile = arrayLyricFiles[0]; + } + + /* Now, For the images we still haven't found, + * look at the image dimensions of the remaining unclassified images. */ + vector arrayImages; + GetImageDirListing( m_sSongDir + "*", arrayImages ); + + for( unsigned i=0; iUserLog( "Graphic file", sPath, "couldn't be loaded: %s", error.c_str() ); + continue; + } + + const int width = img->w; + const int height = img->h; + delete img; + + if( !HasBackground() && width >= 320 && height >= 240 ) + { + m_sBackgroundFile = arrayImages[i]; + continue; + } + + if( !HasBanner() && 100<=width && width<=320 && 50<=height && height<=240 ) + { + m_sBannerFile = arrayImages[i]; + continue; + } + + /* Some songs have overlarge banners. Check if the ratio is reasonable + * (over 2:1; usually over 3:1), and large (not a cdtitle). */ + if( !HasBanner() && width > 200 && float(width) / height > 2.0f ) + { + m_sBannerFile = arrayImages[i]; + continue; + } + + /* Agh. DWI's inline title images are triggering this, resulting in + * kanji, etc., being used as a CDTitle for songs with none. Some + * sample data from random incarnations: + * 42x50 35x50 50x50 144x49 + * It looks like ~50 height is what people use to align to DWI's font. + * + * My tallest CDTitle is 44. Let's cut off in the middle and hope for + * the best. -(who? -aj) */ + /* The proper size of a CDTitle is 64x48 or sometihng. Simfile artists + * typically don't give a shit about this (see Cetaka's fucking banner + * -sized CDTitle). This is also subverted in certain designs (beta + * Mungyodance 3 simfiles, for instance, used the CDTitle to hold + * various information about the song in question). As it stands, + * I'm keeping this code until I figure out wtf to do -aj + */ + if( !HasCDTitle() && width<=100 && height<=48 ) + { + m_sCDTitleFile = arrayImages[i]; + continue; + } + + // Jacket files typically have the same width and height. + if( !HasJacket() && width == height ) + { + m_sJacketFile = arrayImages[i]; + continue; + } + + // Disc images are typically rectangular; make sure we have a banner already. + if( !HasDisc() && (width > height) && HasBanner() ) + { + if( arrayImages[i] != m_sBannerFile ) + m_sDiscFile = arrayImages[i]; + continue; + } + + // CD images are the same as Jackets, typically the same width and height + if( !HasCDImage() && width == height ) + { + m_sCDFile = arrayImages[i]; + continue; + } + } + + } + + // These will be written to cache, for Song::LoadFromSongDir to use later. + m_bHasMusic = HasMusic(); + m_bHasBanner = HasBanner(); + m_bHasBackground = HasBackground(); + + if( HasBanner() ) + BANNERCACHE->CacheBanner( GetBannerPath() ); + /* + if( HasBackground() ) + BANNERCACHE->CacheBackground( GetBackgroundPath() ); + */ + + // If no BGChanges are specified and there are movies in the song directory, then assume + // they are DWI style where the movie begins at beat 0. + if( (!HasBGChanges() && !fromCache) ) + { + vector arrayPossibleMovies; + GetDirListing( m_sSongDir + RString("*.ogv"), arrayPossibleMovies ); + GetDirListing( m_sSongDir + RString("*.avi"), arrayPossibleMovies ); + GetDirListing( m_sSongDir + RString("*.mpg"), arrayPossibleMovies ); + GetDirListing( m_sSongDir + RString("*.mpeg"), arrayPossibleMovies ); + /* Use this->GetBeatFromElapsedTime(0) instead of 0 to start when the + * music starts. */ + if( arrayPossibleMovies.size() == 1 ) + this->AddBackgroundChange(BACKGROUND_LAYER_1, + BackgroundChange(0, + arrayPossibleMovies[0], + "", + 1.f, + SBE_StretchNoLoop)); + } + + + /* Don't allow multiple Steps of the same StepsType and Difficulty (except + * for edits). We should be able to use difficulty names as unique + * identifiers for steps. */ + if( !fromCache ) + { + SongUtil::AdjustDuplicateSteps( this ); + } +} + +void Song::TranslateTitles() +{ + static TitleSubst tsub("Songs"); + + TitleFields title; + title.LoadFromStrings(m_sMainTitle, m_sSubTitle, m_sArtist, + m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); + tsub.Subst( title ); + title.SaveToStrings(m_sMainTitle, m_sSubTitle, m_sArtist, + m_sMainTitleTranslit, m_sSubTitleTranslit, m_sArtistTranslit ); +} + +void Song::ReCalculateRadarValuesAndLastSecond(bool fromCache, bool duringCache) +{ + if( fromCache && this->GetFirstSecond() >= 0 && this->GetLastSecond() > 0 ) + { + // this is loaded from cache, then we just have to calculate the radar values. + for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); + return; + } + + float localFirst = FLT_MAX; // inf + // Make sure we're at least as long as the specified amount below. + float localLast = this->specifiedLastSecond; + + for( unsigned i=0; iCalculateRadarValues( m_fMusicLengthSeconds ); + + // Must initialize before the gotos. + NoteData tempNoteData; + pSteps->GetNoteData( tempNoteData ); + + // calculate lastSecond + + // If it's autogen, then first/last beat will come from the parent. + if( pSteps->IsAutogen() ) + goto wipe_notedata; + + /* Don't calculate with edits unless the song only contains an edit + * chart, like those in Mungyodance 3. Otherwise, edits installed on + * the machine could extend the length of the song. */ + if( pSteps->IsAnEdit() && m_vpSteps.size() > 1 ) + goto wipe_notedata; + + // Don't set first/last beat based on lights. They often start very + // early and end very late. + if( pSteps->m_StepsType == StepsType_lights_cabinet ) + continue; // no need to wipe this. + + /* Many songs have stray, empty song patterns. Ignore them, so they + * don't force the first beat of the whole song to 0. */ + if( tempNoteData.GetLastRow() != 0 ) + { + localFirst = min(localFirst, + pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetFirstBeat())); + localLast = max(localLast, + pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetLastBeat())); + } + wipe_notedata: + if (duringCache) + { + NoteData dummy; + dummy.SetNumTracks(tempNoteData.GetNumTracks()); + pSteps->SetNoteData(dummy); + } + } + + // Yes, for some reason we can have freaky stuff take place here. + this->firstSecond = (localFirst < localLast) ? localFirst : 0; + this->lastSecond = localLast; +} + +// Return whether the song is playable in the given style. +bool Song::SongCompleteForStyle( const Style *st ) const +{ + return HasStepsType( st->m_StepsType ); +} + +bool Song::HasStepsType( StepsType st ) const +{ + return SongUtil::GetOneSteps( this, st ) != NULL; +} + +bool Song::HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const +{ + return SongUtil::GetOneSteps( this, st, dc ) != NULL; +} + +void Song::Save() +{ + LOG->Trace( "Song::SaveToSongFile()" ); + + ReCalculateRadarValuesAndLastSecond(); + TranslateTitles(); + + // Save the new files. These calls make backups on their own. + if( !SaveToSSCFile(GetSongFilePath(), false) ) + return; + SaveToSMFile(); + //SaveToDWIFile(); + SaveToCacheFile(); + + /* We've safely written our files and created backups. Rename non-SM and + * non-DWI files to avoid confusion. */ + vector arrayOldFileNames; + GetDirListing( m_sSongDir + "*.bms", arrayOldFileNames ); + GetDirListing( m_sSongDir + "*.pms", arrayOldFileNames ); + GetDirListing( m_sSongDir + "*.ksf", arrayOldFileNames ); + + for( unsigned i=0; iUserLog( "Song file", sOldPath, "couldn't be backed up." ); + // Don't remove. + } + else + FILEMAN->Remove( sOldPath ); + } +} + +bool Song::SaveToSMFile() +{ + const RString sPath = SetExtension( GetSongFilePath(), "sm" ); + LOG->Trace( "Song::SaveToSMFile(%s)", sPath.c_str() ); + + // If the file exists, make a backup. + if( IsAFile(sPath) ) + FileCopy( sPath, sPath + ".old" ); + + vector vpStepsToSave; + FOREACH_CONST( Steps*, m_vpSteps, s ) + { + Steps *pSteps = *s; + if( pSteps->IsAutogen() ) + continue; // don't write autogen notes + + // Only save steps that weren't loaded from a profile. + if( pSteps->WasLoadedFromProfile() ) + continue; + + vpStepsToSave.push_back( pSteps ); + } + + return NotesWriterSM::Write( sPath, *this, vpStepsToSave ); + +} + +bool Song::SaveToSSCFile( RString sPath, bool bSavingCache ) +{ + RString path = sPath; + if (!bSavingCache) + path = SetExtension(sPath, "ssc"); + + LOG->Trace( "Song::SaveToSSCFile('%s')", path.c_str() ); + + // If the file exists, make a backup. + if( !bSavingCache && IsAFile(path) ) + FileCopy( path, path + ".old" ); + + vector vpStepsToSave; + FOREACH_CONST( Steps*, m_vpSteps, s ) + { + Steps *pSteps = *s; + if( pSteps->IsAutogen() ) + continue; // don't write autogen notes + + // Only save steps that weren't loaded from a profile. + if( pSteps->WasLoadedFromProfile() ) + continue; + + if (!bSavingCache) + pSteps->SetFilename(path); + vpStepsToSave.push_back( pSteps ); + } + + if (bSavingCache) + { + return NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache); + } + + if( !NotesWriterSSC::Write(path, *this, vpStepsToSave, bSavingCache) ) + return false; + + if( g_BackUpAllSongSaves.Get() ) + { + RString sExt = GetExtension( path ); + RString sBackupFile = SetExtension( path, "" ); + + time_t cur_time; + time( &cur_time ); + struct tm now; + localtime_r( &cur_time, &now ); + + sBackupFile += ssprintf( "-%04i-%02i-%02i--%02i-%02i-%02i", + 1900+now.tm_year, now.tm_mon+1, now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec ); + sBackupFile = SetExtension( sBackupFile, sExt ); + sBackupFile += ssprintf( ".old" ); + + if( FileCopy(path, sBackupFile) ) + LOG->Trace( "Backed up %s to %s", path.c_str(), sBackupFile.c_str() ); + else + LOG->Trace( "Failed to back up %s to %s", path.c_str(), sBackupFile.c_str() ); + } + + // Mark these steps saved to disk. + FOREACH( Steps*, vpStepsToSave, s ) + (*s)->SetSavedToDisk( true ); + + return true; +} + +bool Song::SaveToJsonFile( RString sPath ) +{ + LOG->Trace( "Song::SaveToJsonFile('%s')", sPath.c_str() ); + return NotesWriterJson::WriteSong(sPath, *this, true); +} + +bool Song::SaveToCacheFile() +{ + SONGINDEX->AddCacheIndex(m_sSongDir, GetHashForDirectory(m_sSongDir)); + const RString sPath = GetCacheFilePath(); + return SaveToSSCFile(sPath, true); +} + +bool Song::SaveToDWIFile() +{ + const RString sPath = SetExtension( GetSongFilePath(), "dwi" ); + LOG->Trace( "Song::SaveToDWIFile(%s)", sPath.c_str() ); + + // If the file exists, make a backup. + if( IsAFile(sPath) ) + FileCopy( sPath, sPath + ".old" ); + + return NotesWriterDWI::Write( sPath, *this ); +} + + +void Song::AddAutoGenNotes() +{ + bool HasNotes[NUM_StepsType]; + memset( HasNotes, 0, sizeof(HasNotes) ); + for( unsigned i=0; i < m_vpSteps.size(); i++ ) // foreach Steps + { + if( m_vpSteps[i]->IsAutogen() ) + continue; + + StepsType st = m_vpSteps[i]->m_StepsType; + HasNotes[st] = true; + } + + FOREACH_ENUM( StepsType, stMissing ) + { + if( HasNotes[stMissing] ) + continue; + + // If m_bAutogenSteps is disabled, only autogen lights. + if( !PREFSMAN->m_bAutogenSteps && stMissing != StepsType_lights_cabinet ) + continue; + if( !GAMEMAN->GetStepsTypeInfo(stMissing).bAllowAutogen ) + continue; + + // missing Steps of this type + int iNumTracksOfMissing = GAMEMAN->GetStepsTypeInfo(stMissing).iNumTracks; + + // look for closest match + StepsType stBestMatch = StepsType_Invalid; + int iBestTrackDifference = INT_MAX; + + FOREACH_ENUM( StepsType, st ) + { + if( !HasNotes[st] ) + continue; + + // has (non-autogen) Steps of this type + const int iNumTracks = GAMEMAN->GetStepsTypeInfo(st).iNumTracks; + const int iTrackDifference = abs(iNumTracks-iNumTracksOfMissing); + if( iTrackDifference < iBestTrackDifference ) + { + stBestMatch = st; + iBestTrackDifference = iTrackDifference; + } + } + + if( stBestMatch != StepsType_Invalid ) + AutoGen( stMissing, stBestMatch ); + } +} + +void Song::AutoGen( StepsType ntTo, StepsType ntFrom ) +{ + // int iNumTracksOfTo = GAMEMAN->StepsTypeToNumTracks(ntTo); + + for( unsigned int j=0; jm_StepsType == ntFrom ) + { + Steps* pNewNotes = new Steps; + pNewNotes->AutogenFrom( pOriginalNotes, ntTo ); + this->AddSteps( pNewNotes ); + } + } +} + +void Song::RemoveAutoGenNotes() +{ + FOREACH_ENUM( StepsType, st ) + { + for( int j=m_vpStepsByType[st].size()-1; j>=0; j-- ) + { + if( m_vpStepsByType[st][j]->IsAutogen() ) + { + // delete m_vpSteps[j]; // delete below + m_vpStepsByType[st].erase( m_vpStepsByType[st].begin()+j ); + } + } + } + + for( int j=m_vpSteps.size()-1; j>=0; j-- ) + { + if( m_vpSteps[j]->IsAutogen() ) + { + delete m_vpSteps[j]; + m_vpSteps.erase( m_vpSteps.begin()+j ); + } + } +} + +bool Song::IsEasy( StepsType st ) const +{ + /* Very fast songs and songs with wide tempo changes are hard for new + * players, even if they have beginner steps. */ + DisplayBpms bpms; + this->GetDisplayBpms(bpms); + if( bpms.GetMax() >= 250 || bpms.GetMax() - bpms.GetMin() >= 75 ) + return false; + + /* The easy marker indicates which songs a beginner, having selected + * "beginner", can play and actually get a very easy song: if there are + * actual beginner steps, or if the light steps are 1- or 2-foot. */ + const Steps* pBeginnerNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Beginner ); + if( pBeginnerNotes ) + return true; + + const Steps* pEasyNotes = SongUtil::GetStepsByDifficulty( this, st, Difficulty_Easy ); + if( pEasyNotes && pEasyNotes->GetMeter() == 1 ) + return true; + + return false; +} + +bool Song::IsTutorial() const +{ + // A Song is considered a Tutorial if it has only Beginner steps. + FOREACH_CONST( Steps*, m_vpSteps, s ) + { + if( (*s)->m_StepsType == StepsType_lights_cabinet ) + continue; // ignore + if( (*s)->GetDifficulty() != Difficulty_Beginner ) + return false; + } + + return true; +} + +bool Song::HasEdits( StepsType st ) const +{ + for( unsigned i=0; im_StepsType == st && + pSteps->GetDifficulty() == Difficulty_Edit ) + { + return true; + } + } + + return false; +} + +bool Song::NormallyDisplayed() const +{ + return UNLOCKMAN == NULL || !UNLOCKMAN->SongIsLocked(this); +} + +bool Song::ShowInDemonstrationAndRanking() const { return !IsTutorial() && NormallyDisplayed(); } + + +// Hack: see Song::TidyUpData comments. +bool Song::HasMusic() const +{ + // If we have keys, we always have music. + if( m_vsKeysoundFile.size() != 0 ) + return true; + + return m_sMusicFile != "" && IsAFile(GetMusicPath()); +} +bool Song::HasBanner() const +{ + return m_sBannerFile != "" && IsAFile(GetBannerPath()); +} +bool Song::HasInstrumentTrack( InstrumentTrack it ) const +{ + return m_sInstrumentTrackFile[it] != "" && IsAFile(GetInstrumentTrackPath(it)); +} +bool Song::HasLyrics() const +{ + return m_sLyricsFile != "" && IsAFile(GetLyricsPath()); +} +bool Song::HasBackground() const +{ + return m_sBackgroundFile != "" && IsAFile(GetBackgroundPath()); +} +bool Song::HasCDTitle() const +{ + return m_sCDTitleFile != "" && IsAFile(GetCDTitlePath()); +} +bool Song::HasBGChanges() const +{ + FOREACH_BackgroundLayer( i ) + { + if( !GetBackgroundChanges(i).empty() ) + return true; + } + return false; +} +bool Song::HasAttacks() const {return !m_Attacks.empty(); } +bool Song::HasJacket() const +{ + return m_sJacketFile != "" && IsAFile(GetJacketPath()); +} +bool Song::HasDisc() const +{ + return m_sDiscFile != "" && IsAFile(GetDiscPath()); +} +bool Song::HasCDImage() const +{ + return m_sCDFile != "" && IsAFile(GetCDImagePath()); +} +bool Song::HasPreviewVid() const +{ + return m_sPreviewVidFile != "" && IsAFile(GetPreviewVidPath()); +} + +const vector &Song::GetBackgroundChanges( BackgroundLayer bl ) const +{ + return *(m_BackgroundChanges[bl]); +} +vector &Song::GetBackgroundChanges( BackgroundLayer bl ) +{ + return *(m_BackgroundChanges[bl].Get()); +} + +const vector &Song::GetForegroundChanges() const +{ + return *m_ForegroundChanges; +} +vector &Song::GetForegroundChanges() +{ + return *m_ForegroundChanges.Get(); +} + +vector Song::GetChangesToVectorString(const vector & changes) const +{ + vector ret; + FOREACH_CONST( BackgroundChange, changes, bgc ) + { + ret.push_back((*bgc).ToString()); + } + return ret; +} + +vector Song::GetBGChanges1ToVectorString() const +{ + return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_1)); +} + +vector Song::GetBGChanges2ToVectorString() const +{ + return this->GetChangesToVectorString(this->GetBackgroundChanges(BACKGROUND_LAYER_2)); +} + +vector Song::GetFGChanges1ToVectorString() const +{ + return this->GetChangesToVectorString(this->GetForegroundChanges()); +} + +vector Song::GetInstrumentTracksToVectorString() const +{ + vector ret; + FOREACH_ENUM(InstrumentTrack, it) + { + if (this->HasInstrumentTrack(it)) + { + ret.push_back(InstrumentTrackToString(it) + + "=" + + this->m_sInstrumentTrackFile[it]); + } + } + return ret; +} + +RString GetSongAssetPath( RString sPath, const RString &sSongPath ) +{ + if( sPath == "" ) + return RString(); + + /* If there's no path in the file, the file is in the same directory as the + * song. (This is the preferred configuration.) */ + if( sPath.find('/') == string::npos ) + return sSongPath+sPath; + + // The song contains a path; treat it as relative to the top SM directory. + if( sPath.Left(3) == "../" ) + { + // The path begins with "../". Resolve it wrt. the song directory. + sPath = sSongPath + sPath; + } + + CollapsePath( sPath ); + + /* If the path still begins with "../", then there were an unreasonable number + * of them at the beginning of the path. Ignore the path entirely. */ + if( sPath.Left(3) == "../" ) + return RString(); + + return sPath; +} + +/* Note that supplying a path relative to the top-level directory is only for + * compatibility with DWI. We prefer paths relative to the song directory. */ +RString Song::GetMusicPath() const +{ + return GetSongAssetPath( m_sMusicFile, m_sSongDir ); +} + +RString Song::GetInstrumentTrackPath( InstrumentTrack it ) const +{ + return GetSongAssetPath( m_sInstrumentTrackFile[it], m_sSongDir ); +} + +RString Song::GetBannerPath() const +{ + return GetSongAssetPath( m_sBannerFile, m_sSongDir ); +} + +RString Song::GetLyricsPath() const +{ + return GetSongAssetPath( m_sLyricsFile, m_sSongDir ); +} + +RString Song::GetCDTitlePath() const +{ + return GetSongAssetPath( m_sCDTitleFile, m_sSongDir ); +} + +RString Song::GetBackgroundPath() const +{ + return GetSongAssetPath( m_sBackgroundFile, m_sSongDir ); +} + +RString Song::GetJacketPath() const +{ + return GetSongAssetPath( m_sJacketFile, m_sSongDir ); +} + +RString Song::GetDiscPath() const +{ + return GetSongAssetPath( m_sDiscFile, m_sSongDir ); +} + +RString Song::GetCDImagePath() const +{ + return GetSongAssetPath( m_sCDFile, m_sSongDir ); +} + +RString Song::GetPreviewVidPath() const +{ + return GetSongAssetPath( m_sPreviewVidFile, m_sSongDir ); +} + +RString Song::GetDisplayMainTitle() const +{ + if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitMainTitle(); + return m_sMainTitle; +} + +RString Song::GetDisplaySubTitle() const +{ + if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitSubTitle(); + return m_sSubTitle; +} + +RString Song::GetDisplayArtist() const +{ + if(!PREFSMAN->m_bShowNativeLanguage) return GetTranslitArtist(); + return m_sArtist; +} + +RString Song::GetDisplayFullTitle() const +{ + RString Title = GetDisplayMainTitle(); + RString SubTitle = GetDisplaySubTitle(); + + if(!SubTitle.empty()) Title += " " + SubTitle; + return Title; +} + +RString Song::GetTranslitFullTitle() const +{ + RString Title = GetTranslitMainTitle(); + RString SubTitle = GetTranslitSubTitle(); + + if(!SubTitle.empty()) Title += " " + SubTitle; + return Title; +} + +void Song::AddSteps( Steps* pSteps ) +{ + m_vpSteps.push_back( pSteps ); + ASSERT_M( pSteps->m_StepsType < NUM_StepsType, ssprintf("%i", pSteps->m_StepsType) ); + m_vpStepsByType[pSteps->m_StepsType].push_back( pSteps ); +} + +void Song::DeleteSteps( const Steps* pSteps, bool bReAutoGen ) +{ + ASSERT( !pSteps->IsAutogen() ); + + // Avoid any stale Note::parent pointers by removing all AutoGen'd Steps, + // then adding them again. + + if( bReAutoGen ) + RemoveAutoGenNotes(); + + vector &vpSteps = m_vpStepsByType[pSteps->m_StepsType]; + for( int j=vpSteps.size()-1; j>=0; j-- ) + { + if( vpSteps[j] == pSteps ) + { + //delete vpSteps[j]; // delete below + vpSteps.erase( vpSteps.begin()+j ); + break; + } + } + + for( int j=m_vpSteps.size()-1; j>=0; j-- ) + { + if( m_vpSteps[j] == pSteps ) + { + delete m_vpSteps[j]; + m_vpSteps.erase( m_vpSteps.begin()+j ); + break; + } + } + + if( bReAutoGen ) + AddAutoGenNotes(); +} + +bool Song::Matches(RString sGroup, RString sSong) const +{ + if( sGroup.size() && sGroup.CompareNoCase(this->m_sGroupName) != 0) + return false; + + RString sDir = this->GetSongDir(); + sDir.Replace("\\","/"); + vector bits; + split( sDir, "/", bits ); + ASSERT(bits.size() >= 2); // should always have at least two parts + const RString &sLastBit = bits[bits.size()-1]; + + // match on song dir or title (ala DWI) + if( !sSong.CompareNoCase(sLastBit) ) + return true; + if( !sSong.CompareNoCase(this->GetTranslitFullTitle()) ) + return true; + + return false; +} + +/* If apInUse is set, it contains a list of steps which are in use elsewhere, + * and should not be deleted. */ +void Song::FreeAllLoadedFromProfile( ProfileSlot slot, const set *setInUse ) +{ + /* DeleteSteps will remove and recreate autogen notes, which may reorder + * m_vpSteps, so be careful not to skip over entries. */ + vector apToRemove; + for( int s=m_vpSteps.size()-1; s>=0; s-- ) + { + Steps* pSteps = m_vpSteps[s]; + if( !pSteps->WasLoadedFromProfile() ) + continue; + if( slot != ProfileSlot_Invalid && pSteps->GetLoadedFromProfileSlot() != slot ) + continue; + if( setInUse != NULL && setInUse->find(pSteps) != setInUse->end() ) + continue; + apToRemove.push_back( pSteps ); + } + + for( unsigned i = 0; i < apToRemove.size(); ++i ) + this->DeleteSteps( apToRemove[i] ); +} + +void Song::GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const +{ + for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) + vpStepsOut.push_back( pSteps ); + } +} + +int Song::GetNumStepsLoadedFromProfile( ProfileSlot slot ) const +{ + int iCount = 0; + for( unsigned s=0; sGetLoadedFromProfileSlot() == slot ) + iCount++; + } + return iCount; +} + +bool Song::IsEditAlreadyLoaded( Steps* pSteps ) const +{ + ASSERT_M( pSteps->GetDifficulty() == Difficulty_Edit, + ssprintf("The %s chart for %s is no edit, thus it can't be checked for loading.", + DifficultyToString(pSteps->GetDifficulty()).c_str(), + this->m_sMainTitle.c_str())); + + for( unsigned i=0; iGetDifficulty() == Difficulty_Edit && + pOther->m_StepsType == pSteps->m_StepsType && + pOther->GetHash() == pSteps->GetHash() ) + { + return true; + } + } + + return false; +} + +bool Song::IsStepsUsingDifferentTiming(Steps *pSteps) const +{ + return pSteps->m_Timing != this->m_SongTiming; +} + +bool Song::HasSignificantBpmChangesOrStops() const +{ + if( m_SongTiming.HasStops() || m_SongTiming.HasDelays() ) + return true; + + // Don't consider BPM changes that only are only for maintaining sync as + // a real BpmChange. + if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) + { + if( m_fSpecifiedBPMMin != m_fSpecifiedBPMMax ) + return true; + } + else if( m_SongTiming.HasBpmChanges() ) + { + return true; + } + + return false; +} + +float Song::GetStepsSeconds() const +{ + return this->GetLastSecond() - this->GetFirstSecond(); +} + +bool Song::IsLong() const +{ + return !IsMarathon() && m_fMusicLengthSeconds >= g_fLongVerSongSeconds; +} + +bool Song::IsMarathon() const +{ + return m_fMusicLengthSeconds >= g_fMarathonVerSongSeconds; +} + +// lua start +#include "LuaBinding.h" + +/** @brief Allow Lua to have access to the Song. */ +class LunaSong: public Luna +{ +public: + static int GetDisplayFullTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplayFullTitle() ); return 1; + } + static int GetTranslitFullTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitFullTitle() ); return 1; + } + static int GetDisplayMainTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplayMainTitle() ); return 1; + } + static int GetTranslitMainTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitMainTitle() ); return 1; + } + static int GetDisplaySubTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplaySubTitle() ); return 1; + } + static int GetTranslitSubTitle( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitSubTitle() ); return 1; + } + static int GetDisplayArtist( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetDisplayArtist() ); return 1; + } + static int GetTranslitArtist( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetTranslitArtist() ); return 1; + } + static int GetGenre( T* p, lua_State *L ) + { + lua_pushstring(L, p->m_sGenre ); return 1; + } + static int GetOrigin( T* p, lua_State *L ) + { + lua_pushstring(L, p->m_sOrigin ); return 1; + } + static int GetAllSteps( T* p, lua_State *L ) + { + const vector &v = p->GetAllSteps(); + LuaHelpers::CreateTableFromArray( v, L ); + return 1; + } + static int GetStepsByStepsType( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + const vector &v = p->GetStepsByStepsType( st ); + LuaHelpers::CreateTableFromArray( v, L ); + return 1; + } + static int GetSongDir( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetSongDir() ); + return 1; + } + static int GetMusicPath( T* p, lua_State *L ) + { + RString s = p->GetMusicPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetBannerPath( T* p, lua_State *L ) + { + RString s = p->GetBannerPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetBackgroundPath( T* p, lua_State *L ) + { + RString s = p->GetBackgroundPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetPreviewVidPath( T* p, lua_State *L ) + { + RString s = p->GetPreviewVidPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetJacketPath( T* p, lua_State *L ) + { + RString s = p->GetJacketPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetCDImagePath( T* p, lua_State *L ) + { + RString s = p->GetCDImagePath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetDiscPath( T* p, lua_State *L ) + { + RString s = p->GetDiscPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetCDTitlePath( T* p, lua_State *L ) + { + RString s = p->GetCDTitlePath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetLyricsPath( T* p, lua_State *L ) + { + RString s = p->GetLyricsPath(); + if( !s.empty() ) + lua_pushstring(L, s); + else + lua_pushnil(L); + return 1; + } + static int GetSongFilePath( T* p, lua_State *L ) + { + lua_pushstring(L, p->GetSongFilePath() ); + return 1; + } + static int IsTutorial( T* p, lua_State *L ) + { + lua_pushboolean(L, p->IsTutorial()); + return 1; + } + static int IsEnabled( T* p, lua_State *L ) + { + lua_pushboolean(L, p->GetEnabled()); + return 1; + } + static int GetGroupName( T* p, lua_State *L ) + { + lua_pushstring(L, p->m_sGroupName); + return 1; + } + static int MusicLengthSeconds( T* p, lua_State *L ) + { + lua_pushnumber(L, p->m_fMusicLengthSeconds); + return 1; + } + static int GetSampleStart( T* p, lua_State *L ) + { + lua_pushnumber(L, p->m_fMusicSampleStartSeconds); + return 1; + } + static int GetSampleLength( T* p, lua_State *L ) + { + lua_pushnumber(L, p->m_fMusicSampleLengthSeconds); + return 1; + } + static int IsLong( T* p, lua_State *L ) + { + lua_pushboolean(L, p->IsLong()); + return 1; + } + static int IsMarathon( T* p, lua_State *L ) + { + lua_pushboolean(L, p->IsMarathon()); + return 1; + } + static int HasStepsType( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + lua_pushboolean( L, p->HasStepsType(st) ); + return 1; + } + static int HasStepsTypeAndDifficulty( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + Difficulty dc = Enum::Check( L, 2 ); + lua_pushboolean( L, p->HasStepsTypeAndDifficulty(st, dc) ); + return 1; + } + static int IsStepsUsingDifferentTiming(T* p, lua_State *L) + { + lua_pushboolean(L, p->IsStepsUsingDifferentTiming(Luna::check( L, 1, true ))); + return 1; + } + /* TODO: HasStepsTypeAndDifficulty and GetOneSteps should be in + * a SongUtil Lua table and a method of Steps. */ + static int GetOneSteps( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + Difficulty dc = Enum::Check( L, 2 ); + Steps *pSteps = SongUtil::GetOneSteps( p, st, dc ); + if( pSteps ) + pSteps->PushSelf(L); + else + lua_pushnil(L); + return 1; + } + static int GetTimingData( T* p, lua_State *L ) { p->m_SongTiming.PushSelf(L); return 1; } + // has functions + static int HasMusic( T* p, lua_State *L ) { lua_pushboolean(L, p->HasMusic()); return 1; } + static int HasBanner( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBanner()); return 1; } + static int HasBackground( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBackground()); return 1; } + static int HasPreviewVid( T* p, lua_State *L ) { lua_pushboolean(L, p->HasPreviewVid()); return 1; } + static int HasJacket( T* p, lua_State *L ) { lua_pushboolean(L, p->HasJacket()); return 1; } + static int HasDisc( T* p, lua_State *L ) { lua_pushboolean(L, p->HasDisc()); return 1; } + static int HasCDImage( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDImage()); return 1; } + static int HasCDTitle( T* p, lua_State *L ) { lua_pushboolean(L, p->HasCDTitle()); return 1; } + static int HasBGChanges( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBGChanges()); return 1; } + static int HasLyrics( T* p, lua_State *L ) { lua_pushboolean(L, p->HasLyrics()); return 1; } + // functions that AJ loves + static int HasSignificantBPMChangesOrStops( T* p, lua_State *L ) + { + lua_pushboolean(L, p->HasSignificantBpmChangesOrStops()); + return 1; + } + static int HasEdits( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + lua_pushboolean(L, p->HasEdits( st )); + return 1; + } + static int IsEasy( T* p, lua_State *L ) + { + StepsType st = Enum::Check(L, 1); + lua_pushboolean(L, p->IsEasy( st )); + return 1; + } + static int GetStepsSeconds( T* p, lua_State *L ) + { + lua_pushnumber(L, p->GetStepsSeconds()); + return 1; + } + static int NormallyDisplayed( T* p, lua_State *L ) + { + lua_pushboolean(L, p->NormallyDisplayed()); + return 1; + } + static int ShowInDemonstrationAndRanking( T* p, lua_State *L ) + { + lua_pushboolean(L, p->ShowInDemonstrationAndRanking()); + return 1; + } + static int GetFirstSecond(T* p, lua_State *L) + { + lua_pushnumber(L, p->GetFirstSecond()); + return 1; + } + static int GetLastSecond(T* p, lua_State *L) + { + lua_pushnumber(L, p->GetLastSecond()); + return 1; + } + static int GetFirstBeat( T* p, lua_State *L ) + { + lua_pushnumber(L, p->GetFirstBeat()); + return 1; + } + static int GetLastBeat( T* p, lua_State *L ) + { + lua_pushnumber(L, p->GetLastBeat()); + return 1; + } + static int HasAttacks( T* p, lua_State *L ) + { + lua_pushboolean(L, p->HasAttacks()); + return 1; + } + static int GetDisplayBpms( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + float fMin = temp.GetMin(); + float fMax = temp.GetMax(); + vector fBPMs; + fBPMs.push_back( fMin ); + fBPMs.push_back( fMax ); + LuaHelpers::CreateTableFromArray(fBPMs, L); + return 1; + } + static int IsDisplayBpmSecret( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + lua_pushboolean( L, temp.IsSecret() ); + return 1; + } + static int IsDisplayBpmConstant( T* p, lua_State *L ) + { + DisplayBpms temp; + p->GetDisplayBpms(temp); + lua_pushboolean( L, temp.BpmIsConstant() ); + return 1; + } + static int IsDisplayBpmRandom( T* p, lua_State *L ) + { + lua_pushboolean( L, p->m_DisplayBPMType == DISPLAY_BPM_RANDOM ); + return 1; + } + + LunaSong() + { + ADD_METHOD( GetDisplayFullTitle ); + ADD_METHOD( GetTranslitFullTitle ); + ADD_METHOD( GetDisplayMainTitle ); + ADD_METHOD( GetTranslitMainTitle ); + ADD_METHOD( GetDisplaySubTitle ); + ADD_METHOD( GetTranslitSubTitle ); + ADD_METHOD( GetDisplayArtist ); + ADD_METHOD( GetTranslitArtist ); + ADD_METHOD( GetGenre ); + ADD_METHOD( GetOrigin ); + ADD_METHOD( GetAllSteps ); + ADD_METHOD( GetStepsByStepsType ); + ADD_METHOD( GetSongDir ); + ADD_METHOD( GetMusicPath ); + ADD_METHOD( GetBannerPath ); + ADD_METHOD( GetBackgroundPath ); + ADD_METHOD( GetJacketPath ); + ADD_METHOD( GetCDImagePath ); + ADD_METHOD( GetDiscPath ); + ADD_METHOD( GetCDTitlePath ); + ADD_METHOD( GetLyricsPath ); + ADD_METHOD( GetSongFilePath ); + ADD_METHOD( IsTutorial ); + ADD_METHOD( IsEnabled ); + ADD_METHOD( GetGroupName ); + ADD_METHOD( MusicLengthSeconds ); + ADD_METHOD( GetSampleStart ); + ADD_METHOD( GetSampleLength ); + ADD_METHOD( IsLong ); + ADD_METHOD( IsMarathon ); + ADD_METHOD( HasStepsType ); + ADD_METHOD( HasStepsTypeAndDifficulty ); + ADD_METHOD( GetOneSteps ); + ADD_METHOD( GetTimingData ); + ADD_METHOD( HasMusic ); + ADD_METHOD( HasBanner ); + ADD_METHOD( HasBackground ); + ADD_METHOD( HasJacket ); + ADD_METHOD( HasCDImage ); + ADD_METHOD( HasDisc ); + ADD_METHOD( HasCDTitle ); + ADD_METHOD( HasBGChanges ); + ADD_METHOD( HasLyrics ); + ADD_METHOD( HasSignificantBPMChangesOrStops ); + ADD_METHOD( HasEdits ); + ADD_METHOD( IsEasy ); + ADD_METHOD( GetStepsSeconds ); + ADD_METHOD( NormallyDisplayed ); + ADD_METHOD( GetFirstBeat ); + ADD_METHOD( GetFirstSecond ); + ADD_METHOD( GetLastBeat ); + ADD_METHOD( GetLastSecond ); + ADD_METHOD( HasAttacks ); + ADD_METHOD( GetDisplayBpms ); + ADD_METHOD( IsDisplayBpmSecret ); + ADD_METHOD( IsDisplayBpmConstant ); + ADD_METHOD( IsDisplayBpmRandom ); + ADD_METHOD( IsStepsUsingDifferentTiming ); + ADD_METHOD( ShowInDemonstrationAndRanking ); + ADD_METHOD( HasPreviewVid ); + ADD_METHOD( GetPreviewVidPath ); + } +}; + +LUA_REGISTER_CLASS( Song ) +// lua end + + +/* + * (c) 2001-2004 Chris Danford, Glenn Maynard + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/Song.h b/src/Song.h index f06102adbe..ef32eec02f 100644 --- a/src/Song.h +++ b/src/Song.h @@ -1,475 +1,479 @@ -#ifndef SONG_H -#define SONG_H - -#include "Attack.h" -#include "TimingData.h" -#include "Difficulty.h" -#include "EnumHelper.h" -#include "RageUtil_AutoPtr.h" -#include "RageUtil_CachedObject.h" -#include "RageTypes.h" -#include "Steps.h" -#include - -class Style; -class StepsID; -struct lua_State; -struct BackgroundChange; - -void FixupPath( RString &path, const RString &sSongPath ); -RString GetSongAssetPath( RString sPath, const RString &sSongPath ); - -/** @brief The version of the .ssc file format. */ -const static float STEPFILE_VERSION_NUMBER = 0.81f; - -/** @brief How many edits for this song can each profile have? */ -const int MAX_EDITS_PER_SONG_PER_PROFILE = 15; -/** @brief How many edits for this song can be available? */ -const int MAX_EDITS_PER_SONG = MAX_EDITS_PER_SONG_PER_PROFILE * NUM_ProfileSlot; - -extern const int FILE_CACHE_VERSION; - -/** @brief The different background layers available. */ -enum BackgroundLayer -{ - BACKGROUND_LAYER_1, - BACKGROUND_LAYER_2, - //BACKGROUND_LAYER_3, // StepCollection get - NUM_BackgroundLayer, - BACKGROUND_LAYER_Invalid -}; - -/** @brief A custom foreach loop for the different background layers. */ -#define FOREACH_BackgroundLayer( bl ) FOREACH_ENUM( BackgroundLayer, bl ) - -/** @brief Different instrument tracks for band type games. */ -enum InstrumentTrack -{ - InstrumentTrack_Guitar, - InstrumentTrack_Rhythm, - InstrumentTrack_Bass, - NUM_InstrumentTrack, - InstrumentTrack_Invalid -}; -const RString& InstrumentTrackToString( InstrumentTrack it ); -InstrumentTrack StringToInstrumentTrack( const RString& s ); - -/** @brief The collection of lyrics for the Song. */ -struct LyricSegment -{ - float m_fStartTime; /** @brief When does the lyric show up? */ - RString m_sLyric; /** @brief The lyrics themselves. */ - RageColor m_Color; /** @brief The color of the lyrics. */ -}; - -/** @brief Holds all music metadata and steps for one song. */ -class Song -{ - RString m_sSongDir; -public: - void SetSongDir( const RString sDir ) { m_sSongDir = sDir; } - RString GetSongDir() { return m_sSongDir; } - - /** @brief When should this song be displayed in the music wheel? */ - enum SelectionDisplay - { - SHOW_ALWAYS, /**< always show on the wheel. */ - SHOW_NEVER /**< never show on the wheel (unless song hiding is turned off). */ - } m_SelectionDisplay; - - Song(); - ~Song(); - void Reset(); - void DetachSteps(); - - /** - * @brief Load a song from the chosen directory. - * - * This assumes that there is no song present right now. - * @param sDir the song directory from which to load. */ - bool LoadFromSongDir( RString sDir ); - // This one takes the effort to reuse Steps pointers as best as it can - bool ReloadFromSongDir( RString sDir ); - - /** - * @brief Call this after loading a song to clean up invalid data. - * @param fromCache was this data loaded from the cache file? - * @param duringCache was this data loaded during the cache process? */ - void TidyUpData( bool fromCache = false, bool duringCache = false ); - - /** - * @brief Get the new radar values, and determine the last second at the same time. - * This is called by TidyUpData, after saving the Song. - * @param fromCache was this data loaded from the cache file? - * @param duringCache was this data loaded during the cache process? */ - void ReCalculateRadarValuesAndLastSecond(bool fromCache = false, bool duringCache = false); - /** - * @brief Translate any titles that aren't in english. - * This is called by TidyUpData. */ - void TranslateTitles(); - - /** - * @brief Save to the new SSC file format. - * @param sPath the path where we're saving the file. - * @param bSavingCache a flag to determine if we're saving cache data. - */ - bool SaveToSSCFile( RString sPath, bool bSavingCache ); - /** @brief Save to the SSC and SM files no matter what. */ - void Save(); - /** - * @brief Save the current Song to a cache file using the preferred format. - * @return its success or failure. */ - bool SaveToCacheFile(); - /** - * @brief Save the current Song to a SM file. - * @return its success or failure. */ - bool SaveToSMFile(); - /** - * @brief Save the current Song to a DWI file if possible. - * @return its success or failure. */ - bool SaveToDWIFile(); - - const RString &GetSongFilePath() const; - RString GetCacheFilePath() const; - - void AddAutoGenNotes(); - /** - * @brief Automatically generate steps from one type to another. - * @param ntTo the StepsType we're making. - * @param ntFrom the StepsType we're generating from. - */ - void AutoGen( StepsType ntTo, StepsType ntFrom ); - void RemoveAutoGenNotes(); - - // Directory this song data came from: - const RString &GetSongDir() const { return m_sSongDir; } - - /** - * @brief Filename associated with this file. - * This will always have a .SSC extension. If we loaded a .SSC, this will - * point to it, but if we loaded any other type, this will point to - * a generated .SSC filename. */ - RString m_sSongFileName; - - /** @brief The group this Song is in. */ - RString m_sGroupName; - - /** - * @brief the Profile this came from. - * This is ProfileSlot_Invalid if it wasn't loaded from a profile. */ - ProfileSlot m_LoadedFromProfile; - /** @brief Is the song file itself a symlink to another file? */ - bool m_bIsSymLink; - bool m_bEnabled; - - /** @brief The title of the Song. */ - RString m_sMainTitle; - /** @brief The subtitle of the Song, if it exists. */ - RString m_sSubTitle; - /** @brief The artist of the Song, if it exists. */ - RString m_sArtist; - /** @brief The transliterated title of the Song, if it exists. */ - RString m_sMainTitleTranslit; - /** @brief The transliterated subtitle of the Song, if it exists. */ - RString m_sSubTitleTranslit; - /** @brief The transliterated artist of the Song, if it exists. */ - RString m_sArtistTranslit; - - /* If PREFSMAN->m_bShowNative is off, these are the same as GetTranslit* - * below. Otherwise, they return the main titles. */ - RString GetDisplayMainTitle() const; - RString GetDisplaySubTitle() const; - RString GetDisplayArtist() const; - - /** - * @brief Retrieve the transliterated title, or the main title if there is no translit. - * @return the proper title. */ - RString GetTranslitMainTitle() const - { - return m_sMainTitleTranslit.size()? m_sMainTitleTranslit: m_sMainTitle; - } - /** - * @brief Retrieve the transliterated subtitle, or the main subtitle if there is no translit. - * @return the proper subtitle. */ - RString GetTranslitSubTitle() const - { - return m_sSubTitleTranslit.size()? m_sSubTitleTranslit: m_sSubTitle; - } - /** - * @brief Retrieve the transliterated artist, or the main artist if there is no translit. - * @return the proper artist. */ - RString GetTranslitArtist() const - { - return m_sArtistTranslit.size()? m_sArtistTranslit:m_sArtist; - } - - // "title subtitle" - RString GetDisplayFullTitle() const; - RString GetTranslitFullTitle() const; - - /** @brief The version of the song/file. */ - float m_fVersion; - /** @brief The genre of the song/file. */ - RString m_sGenre; - - /** - * @brief The person who worked with the song file who should be credited. - * This is read and saved, but never actually used. */ - RString m_sCredit; - - RString m_sOrigin; // song origin (for .ssc format) - - RString m_sMusicFile; - RString m_sInstrumentTrackFile[NUM_InstrumentTrack]; - - /** @brief The length of the music file. */ - float m_fMusicLengthSeconds; - float m_fMusicSampleStartSeconds; - float m_fMusicSampleLengthSeconds; - DisplayBPM m_DisplayBPMType; - float m_fSpecifiedBPMMin; - float m_fSpecifiedBPMMax; // if a range, then Min != Max - - RString m_sBannerFile; // typically a 16:5 ratio graphic (e.g. 256x80) - RString m_sJacketFile; // typically square (e.g. 192x192, 256x256) - RString m_sCDFile; // square (e.g. 128x128 [DDR 1st-3rd]) - RString m_sDiscFile; // rectangular (e.g. 256x192 [Pump], 200x150 [MGD3]) - /** @brief The location of the lyrics file, if it exists. */ - RString m_sLyricsFile; - RString m_sBackgroundFile; - RString m_sCDTitleFile; - RString m_sPreviewVidFile; - - AttackArray m_Attacks; - vector m_sAttackString; - - RString GetMusicPath() const; - RString GetInstrumentTrackPath( InstrumentTrack it ) const; - RString GetBannerPath() const; - RString GetJacketPath() const; - RString GetCDImagePath() const; - RString GetDiscPath() const; - RString GetLyricsPath() const; - RString GetBackgroundPath() const; - RString GetCDTitlePath() const; - RString GetPreviewVidPath() const; - - // For loading only: - bool m_bHasMusic, m_bHasBanner, m_bHasBackground; - - bool HasMusic() const; - bool HasInstrumentTrack( InstrumentTrack it ) const; - /** - * @brief Does this song have a banner? - * @return true if it does, false otherwise. */ - bool HasBanner() const; - /** - * @brief Does this song have a background image? - * @return true if it does, false otherwise. */ - bool HasBackground() const; - bool HasJacket() const; - bool HasCDImage() const; - bool HasDisc() const; - bool HasCDTitle() const; - //bool HasMovieBackground() const; - bool HasBGChanges() const; - bool HasLyrics() const; - bool HasAttacks() const; - bool HasPreviewVid() const; - - bool Matches(RString sGroup, RString sSong) const; - - /** @brief The Song's TimingData. */ - TimingData m_SongTiming; - - float GetFirstBeat() const; - float GetFirstSecond() const; - float GetLastBeat() const; - float GetLastSecond() const; - float GetSpecifiedLastBeat() const; - float GetSpecifiedLastSecond() const; - - void SetFirstSecond(const float f); - void SetLastSecond(const float f); - void SetSpecifiedLastSecond(const float f); - - typedef vector VBackgroundChange; -private: - /** @brief The first second that a note is hit. */ - float firstSecond; - /** @brief The last second that a note is hit. */ - float lastSecond; - /** @brief The last second of the song for playing purposes. */ - float specifiedLastSecond; - /** - * @brief The background changes (sorted by layer) that are for this Song. - * This uses an AutoPtr instead of a raw pointer so that the - * auto gen'd copy constructor works correctly. - * This must be sorted before gameplay. */ - AutoPtrCopyOnWrite m_BackgroundChanges[NUM_BackgroundLayer]; - /** - * @brief The foreground changes that are for this Song. - * This uses an AutoPtr instead of a raw pointer so that the - * auto gen'd copy constructor works correctly. - * This must be sorted before gameplay. */ - AutoPtrCopyOnWrite m_ForegroundChanges; - - vector GetChangesToVectorString(const vector & changes) const; -public: - const vector &GetBackgroundChanges( BackgroundLayer bl ) const; - vector &GetBackgroundChanges( BackgroundLayer bl ); - const vector &GetForegroundChanges() const; - vector &GetForegroundChanges(); - - vector GetBGChanges1ToVectorString() const; - vector GetBGChanges2ToVectorString() const; - vector GetFGChanges1ToVectorString() const; - - vector GetInstrumentTracksToVectorString() const; - - /** - * @brief The list of LyricSegments. - * This must be sorted before gameplay. */ - vector m_LyricSegments; - -/* [splittiming] - void AddBPMSegment( const BPMSegment &seg ) { m_Timing.AddBPMSegment( seg ); } - void AddStopSegment( const StopSegment &seg ) { m_Timing.AddStopSegment( seg ); } - void AddWarpSegment( const WarpSegment &seg ) { m_Timing.AddWarpSegment( seg ); } -*/ - void AddBackgroundChange( BackgroundLayer blLayer, BackgroundChange seg ); - void AddForegroundChange( BackgroundChange seg ); - void AddLyricSegment( LyricSegment seg ); - - void GetDisplayBpms( DisplayBpms &AddTo ) const; - const BackgroundChange &GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const; - -/* [splittiming] - float GetBPMAtBeat( float fBeat ) const { return m_Timing.GetBPMAtBeat( fBeat ); } - void SetBPMAtBeat( float fBeat, float fBPM ) { m_Timing.SetBPMAtBeat( fBeat, fBPM ); } - BPMSegment& GetBPMSegmentAtBeat( float fBeat ) { return m_Timing.GetBPMSegmentAtBeat( fBeat ); } -*/ - - Steps *CreateSteps(); - void InitSteps(Steps *pSteps); - - /* [splittiming] - float SongGetBeatFromElapsedTime( float fElapsedTime ) const - { - return m_SongTiming.GetBeatFromElapsedTime( fElapsedTime ); - } - float StepsGetBeatFromElapsedTime( float fElapsedTime, const Steps &steps ) const - { - return steps.m_Timing.GetBeatFromElapsedTime( fElapsedTime ); - } - - float SongGetElapsedTimeFromBeat( float fBeat ) const - { - return m_SongTiming.GetElapsedTimeFromBeat( fBeat ); - } - float StepsGetElapsedTimeFromBeat( float fBeat, const Steps &steps ) const - { - return steps.m_Timing.GetElapsedTimeFromBeat( fBeat ); - } - */ - - /* [splittiming] - float GetBeatFromElapsedTime( float fElapsedTime ) const - { - return m_Timing.GetBeatFromElapsedTime( fElapsedTime ); - } - float GetElapsedTimeFromBeat( float fBeat ) const { return m_Timing.GetElapsedTimeFromBeat( fBeat ); } - */ - - bool HasSignificantBpmChangesOrStops() const; - float GetStepsSeconds() const; - bool IsLong() const; - bool IsMarathon() const; - - bool SongCompleteForStyle( const Style *st ) const; - bool HasStepsType( StepsType st ) const; - bool HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const; - const vector& GetAllSteps() const { return m_vpSteps; } - const vector& GetStepsByStepsType( StepsType st ) const { return m_vpStepsByType[st]; } - bool IsEasy( StepsType st ) const; - bool IsTutorial() const; - bool HasEdits( StepsType st ) const; - - void SetEnabled( bool b ) { m_bEnabled = b; } - bool GetEnabled() const { return m_bEnabled; } - /** - * @brief Determine if the song should be shown on the MusicWheel normally. - * Songs that are not displayed normally may still be available during - * random selection, extra stages, or other special conditions. - * @return true if displayed normally, false otherwise. */ - bool NormallyDisplayed() const; - bool ShowInDemonstrationAndRanking() const; - - /** - * @brief Add the chosen Steps to the Song. - * We are responsible for deleting the memory pointed to by pSteps! - * @param pSteps the new steps. */ - void AddSteps( Steps* pSteps ); - void DeleteSteps( const Steps* pSteps, bool bReAutoGen = true ); - - void FreeAllLoadedFromProfile( ProfileSlot slot = ProfileSlot_Invalid, const set *setInUse = NULL ); - bool WasLoadedFromProfile() const { return m_LoadedFromProfile != ProfileSlot_Invalid; } - void GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const; - int GetNumStepsLoadedFromProfile( ProfileSlot slot ) const; - bool IsEditAlreadyLoaded( Steps* pSteps ) const; - - bool IsStepsUsingDifferentTiming(Steps *pSteps ) const; - - /** - * @brief An array of keysound file names (e.g. "beep.wav"). - * The index in this array corresponds to the index in TapNote. - * If you change the index in here, you must change all NoteData too. - * Any note that doesn't have a value in the range of this array - * means "this note doesn't have a keysound". */ - vector m_vsKeysoundFile; - - CachedObject m_CachedObject; - - RString GetAttackString() const - { - return join(":", this->m_sAttackString); - } - - // Lua - void PushSelf( lua_State *L ); - -private: - /** @brief the Steps that belong to this Song. */ - vector m_vpSteps; - /** @brief the Steps of a particular StepsType that belong to this Song. */ - vector m_vpStepsByType[NUM_StepsType]; -}; - -#endif - -/** - * @file - * @author Chris Danford, Glenn Maynard (c) 2001-2004 - * @section LICENSE - * All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, and/or sell copies of the Software, and to permit persons to - * whom the Software is furnished to do so, provided that the above - * copyright notice(s) and this permission notice appear in all copies of - * the Software and that both the above copyright notice(s) and this - * permission notice appear in supporting documentation. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF - * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS - * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT - * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS - * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ +#ifndef SONG_H +#define SONG_H + +#include "Attack.h" +#include "TimingData.h" +#include "Difficulty.h" +#include "EnumHelper.h" +#include "RageUtil_AutoPtr.h" +#include "RageUtil_CachedObject.h" +#include "RageTypes.h" +#include "Steps.h" +#include + +class Style; +class StepsID; +struct lua_State; +struct BackgroundChange; + +void FixupPath( RString &path, const RString &sSongPath ); +RString GetSongAssetPath( RString sPath, const RString &sSongPath ); + +/** @brief The version of the .ssc file format. */ +const static float STEPFILE_VERSION_NUMBER = 0.81f; + +/** @brief How many edits for this song can each profile have? */ +const int MAX_EDITS_PER_SONG_PER_PROFILE = 15; +/** @brief How many edits for this song can be available? */ +const int MAX_EDITS_PER_SONG = MAX_EDITS_PER_SONG_PER_PROFILE * NUM_ProfileSlot; + +extern const int FILE_CACHE_VERSION; + +/** @brief The different background layers available. */ +enum BackgroundLayer +{ + BACKGROUND_LAYER_1, + BACKGROUND_LAYER_2, + //BACKGROUND_LAYER_3, // StepCollection get + NUM_BackgroundLayer, + BACKGROUND_LAYER_Invalid +}; + +/** @brief A custom foreach loop for the different background layers. */ +#define FOREACH_BackgroundLayer( bl ) FOREACH_ENUM( BackgroundLayer, bl ) + +/** @brief Different instrument tracks for band type games. */ +enum InstrumentTrack +{ + InstrumentTrack_Guitar, + InstrumentTrack_Rhythm, + InstrumentTrack_Bass, + NUM_InstrumentTrack, + InstrumentTrack_Invalid +}; +const RString& InstrumentTrackToString( InstrumentTrack it ); +InstrumentTrack StringToInstrumentTrack( const RString& s ); + +/** @brief The collection of lyrics for the Song. */ +struct LyricSegment +{ + float m_fStartTime; /** @brief When does the lyric show up? */ + RString m_sLyric; /** @brief The lyrics themselves. */ + RageColor m_Color; /** @brief The color of the lyrics. */ +}; + +/** @brief Holds all music metadata and steps for one song. */ +class Song +{ + RString m_sSongDir; +public: + void SetSongDir( const RString sDir ) { m_sSongDir = sDir; } + RString GetSongDir() { return m_sSongDir; } + + /** @brief When should this song be displayed in the music wheel? */ + enum SelectionDisplay + { + SHOW_ALWAYS, /**< always show on the wheel. */ + SHOW_NEVER /**< never show on the wheel (unless song hiding is turned off). */ + } m_SelectionDisplay; + + Song(); + ~Song(); + void Reset(); + void DetachSteps(); + + /** + * @brief Load a song from the chosen directory. + * + * This assumes that there is no song present right now. + * @param sDir the song directory from which to load. */ + bool LoadFromSongDir( RString sDir ); + // This one takes the effort to reuse Steps pointers as best as it can + bool ReloadFromSongDir( RString sDir ); + + /** + * @brief Call this after loading a song to clean up invalid data. + * @param fromCache was this data loaded from the cache file? + * @param duringCache was this data loaded during the cache process? */ + void TidyUpData( bool fromCache = false, bool duringCache = false ); + + /** + * @brief Get the new radar values, and determine the last second at the same time. + * This is called by TidyUpData, after saving the Song. + * @param fromCache was this data loaded from the cache file? + * @param duringCache was this data loaded during the cache process? */ + void ReCalculateRadarValuesAndLastSecond(bool fromCache = false, bool duringCache = false); + /** + * @brief Translate any titles that aren't in english. + * This is called by TidyUpData. */ + void TranslateTitles(); + + /** + * @brief Save to the new SSC file format. + * @param sPath the path where we're saving the file. + * @param bSavingCache a flag to determine if we're saving cache data. + */ + bool SaveToSSCFile( RString sPath, bool bSavingCache ); + /** @brief Save to the SSC and SM files no matter what. */ + void Save(); + /** + * @brief Save the current Song to a JSON file. + * @return its success or failure. */ + bool SaveToJsonFile( RString sPath ); + /** + * @brief Save the current Song to a cache file using the preferred format. + * @return its success or failure. */ + bool SaveToCacheFile(); + /** + * @brief Save the current Song to a SM file. + * @return its success or failure. */ + bool SaveToSMFile(); + /** + * @brief Save the current Song to a DWI file if possible. + * @return its success or failure. */ + bool SaveToDWIFile(); + + const RString &GetSongFilePath() const; + RString GetCacheFilePath() const; + + void AddAutoGenNotes(); + /** + * @brief Automatically generate steps from one type to another. + * @param ntTo the StepsType we're making. + * @param ntFrom the StepsType we're generating from. + */ + void AutoGen( StepsType ntTo, StepsType ntFrom ); + void RemoveAutoGenNotes(); + + // Directory this song data came from: + const RString &GetSongDir() const { return m_sSongDir; } + + /** + * @brief Filename associated with this file. + * This will always have a .SSC extension. If we loaded a .SSC, this will + * point to it, but if we loaded any other type, this will point to + * a generated .SSC filename. */ + RString m_sSongFileName; + + /** @brief The group this Song is in. */ + RString m_sGroupName; + + /** + * @brief the Profile this came from. + * This is ProfileSlot_Invalid if it wasn't loaded from a profile. */ + ProfileSlot m_LoadedFromProfile; + /** @brief Is the song file itself a symlink to another file? */ + bool m_bIsSymLink; + bool m_bEnabled; + + /** @brief The title of the Song. */ + RString m_sMainTitle; + /** @brief The subtitle of the Song, if it exists. */ + RString m_sSubTitle; + /** @brief The artist of the Song, if it exists. */ + RString m_sArtist; + /** @brief The transliterated title of the Song, if it exists. */ + RString m_sMainTitleTranslit; + /** @brief The transliterated subtitle of the Song, if it exists. */ + RString m_sSubTitleTranslit; + /** @brief The transliterated artist of the Song, if it exists. */ + RString m_sArtistTranslit; + + /* If PREFSMAN->m_bShowNative is off, these are the same as GetTranslit* + * below. Otherwise, they return the main titles. */ + RString GetDisplayMainTitle() const; + RString GetDisplaySubTitle() const; + RString GetDisplayArtist() const; + + /** + * @brief Retrieve the transliterated title, or the main title if there is no translit. + * @return the proper title. */ + RString GetTranslitMainTitle() const + { + return m_sMainTitleTranslit.size()? m_sMainTitleTranslit: m_sMainTitle; + } + /** + * @brief Retrieve the transliterated subtitle, or the main subtitle if there is no translit. + * @return the proper subtitle. */ + RString GetTranslitSubTitle() const + { + return m_sSubTitleTranslit.size()? m_sSubTitleTranslit: m_sSubTitle; + } + /** + * @brief Retrieve the transliterated artist, or the main artist if there is no translit. + * @return the proper artist. */ + RString GetTranslitArtist() const + { + return m_sArtistTranslit.size()? m_sArtistTranslit:m_sArtist; + } + + // "title subtitle" + RString GetDisplayFullTitle() const; + RString GetTranslitFullTitle() const; + + /** @brief The version of the song/file. */ + float m_fVersion; + /** @brief The genre of the song/file. */ + RString m_sGenre; + + /** + * @brief The person who worked with the song file who should be credited. + * This is read and saved, but never actually used. */ + RString m_sCredit; + + RString m_sOrigin; // song origin (for .ssc format) + + RString m_sMusicFile; + RString m_sInstrumentTrackFile[NUM_InstrumentTrack]; + + /** @brief The length of the music file. */ + float m_fMusicLengthSeconds; + float m_fMusicSampleStartSeconds; + float m_fMusicSampleLengthSeconds; + DisplayBPM m_DisplayBPMType; + float m_fSpecifiedBPMMin; + float m_fSpecifiedBPMMax; // if a range, then Min != Max + + RString m_sBannerFile; // typically a 16:5 ratio graphic (e.g. 256x80) + RString m_sJacketFile; // typically square (e.g. 192x192, 256x256) + RString m_sCDFile; // square (e.g. 128x128 [DDR 1st-3rd]) + RString m_sDiscFile; // rectangular (e.g. 256x192 [Pump], 200x150 [MGD3]) + /** @brief The location of the lyrics file, if it exists. */ + RString m_sLyricsFile; + RString m_sBackgroundFile; + RString m_sCDTitleFile; + RString m_sPreviewVidFile; + + AttackArray m_Attacks; + vector m_sAttackString; + + RString GetMusicPath() const; + RString GetInstrumentTrackPath( InstrumentTrack it ) const; + RString GetBannerPath() const; + RString GetJacketPath() const; + RString GetCDImagePath() const; + RString GetDiscPath() const; + RString GetLyricsPath() const; + RString GetBackgroundPath() const; + RString GetCDTitlePath() const; + RString GetPreviewVidPath() const; + + // For loading only: + bool m_bHasMusic, m_bHasBanner, m_bHasBackground; + + bool HasMusic() const; + bool HasInstrumentTrack( InstrumentTrack it ) const; + /** + * @brief Does this song have a banner? + * @return true if it does, false otherwise. */ + bool HasBanner() const; + /** + * @brief Does this song have a background image? + * @return true if it does, false otherwise. */ + bool HasBackground() const; + bool HasJacket() const; + bool HasCDImage() const; + bool HasDisc() const; + bool HasCDTitle() const; + //bool HasMovieBackground() const; + bool HasBGChanges() const; + bool HasLyrics() const; + bool HasAttacks() const; + bool HasPreviewVid() const; + + bool Matches(RString sGroup, RString sSong) const; + + /** @brief The Song's TimingData. */ + TimingData m_SongTiming; + + float GetFirstBeat() const; + float GetFirstSecond() const; + float GetLastBeat() const; + float GetLastSecond() const; + float GetSpecifiedLastBeat() const; + float GetSpecifiedLastSecond() const; + + void SetFirstSecond(const float f); + void SetLastSecond(const float f); + void SetSpecifiedLastSecond(const float f); + + typedef vector VBackgroundChange; +private: + /** @brief The first second that a note is hit. */ + float firstSecond; + /** @brief The last second that a note is hit. */ + float lastSecond; + /** @brief The last second of the song for playing purposes. */ + float specifiedLastSecond; + /** + * @brief The background changes (sorted by layer) that are for this Song. + * This uses an AutoPtr instead of a raw pointer so that the + * auto gen'd copy constructor works correctly. + * This must be sorted before gameplay. */ + AutoPtrCopyOnWrite m_BackgroundChanges[NUM_BackgroundLayer]; + /** + * @brief The foreground changes that are for this Song. + * This uses an AutoPtr instead of a raw pointer so that the + * auto gen'd copy constructor works correctly. + * This must be sorted before gameplay. */ + AutoPtrCopyOnWrite m_ForegroundChanges; + + vector GetChangesToVectorString(const vector & changes) const; +public: + const vector &GetBackgroundChanges( BackgroundLayer bl ) const; + vector &GetBackgroundChanges( BackgroundLayer bl ); + const vector &GetForegroundChanges() const; + vector &GetForegroundChanges(); + + vector GetBGChanges1ToVectorString() const; + vector GetBGChanges2ToVectorString() const; + vector GetFGChanges1ToVectorString() const; + + vector GetInstrumentTracksToVectorString() const; + + /** + * @brief The list of LyricSegments. + * This must be sorted before gameplay. */ + vector m_LyricSegments; + +/* [splittiming] + void AddBPMSegment( const BPMSegment &seg ) { m_Timing.AddBPMSegment( seg ); } + void AddStopSegment( const StopSegment &seg ) { m_Timing.AddStopSegment( seg ); } + void AddWarpSegment( const WarpSegment &seg ) { m_Timing.AddWarpSegment( seg ); } +*/ + void AddBackgroundChange( BackgroundLayer blLayer, BackgroundChange seg ); + void AddForegroundChange( BackgroundChange seg ); + void AddLyricSegment( LyricSegment seg ); + + void GetDisplayBpms( DisplayBpms &AddTo ) const; + const BackgroundChange &GetBackgroundAtBeat( BackgroundLayer iLayer, float fBeat ) const; + +/* [splittiming] + float GetBPMAtBeat( float fBeat ) const { return m_Timing.GetBPMAtBeat( fBeat ); } + void SetBPMAtBeat( float fBeat, float fBPM ) { m_Timing.SetBPMAtBeat( fBeat, fBPM ); } + BPMSegment& GetBPMSegmentAtBeat( float fBeat ) { return m_Timing.GetBPMSegmentAtBeat( fBeat ); } +*/ + + Steps *CreateSteps(); + void InitSteps(Steps *pSteps); + + /* [splittiming] + float SongGetBeatFromElapsedTime( float fElapsedTime ) const + { + return m_SongTiming.GetBeatFromElapsedTime( fElapsedTime ); + } + float StepsGetBeatFromElapsedTime( float fElapsedTime, const Steps &steps ) const + { + return steps.m_Timing.GetBeatFromElapsedTime( fElapsedTime ); + } + + float SongGetElapsedTimeFromBeat( float fBeat ) const + { + return m_SongTiming.GetElapsedTimeFromBeat( fBeat ); + } + float StepsGetElapsedTimeFromBeat( float fBeat, const Steps &steps ) const + { + return steps.m_Timing.GetElapsedTimeFromBeat( fBeat ); + } + */ + + /* [splittiming] + float GetBeatFromElapsedTime( float fElapsedTime ) const + { + return m_Timing.GetBeatFromElapsedTime( fElapsedTime ); + } + float GetElapsedTimeFromBeat( float fBeat ) const { return m_Timing.GetElapsedTimeFromBeat( fBeat ); } + */ + + bool HasSignificantBpmChangesOrStops() const; + float GetStepsSeconds() const; + bool IsLong() const; + bool IsMarathon() const; + + bool SongCompleteForStyle( const Style *st ) const; + bool HasStepsType( StepsType st ) const; + bool HasStepsTypeAndDifficulty( StepsType st, Difficulty dc ) const; + const vector& GetAllSteps() const { return m_vpSteps; } + const vector& GetStepsByStepsType( StepsType st ) const { return m_vpStepsByType[st]; } + bool IsEasy( StepsType st ) const; + bool IsTutorial() const; + bool HasEdits( StepsType st ) const; + + void SetEnabled( bool b ) { m_bEnabled = b; } + bool GetEnabled() const { return m_bEnabled; } + /** + * @brief Determine if the song should be shown on the MusicWheel normally. + * Songs that are not displayed normally may still be available during + * random selection, extra stages, or other special conditions. + * @return true if displayed normally, false otherwise. */ + bool NormallyDisplayed() const; + bool ShowInDemonstrationAndRanking() const; + + /** + * @brief Add the chosen Steps to the Song. + * We are responsible for deleting the memory pointed to by pSteps! + * @param pSteps the new steps. */ + void AddSteps( Steps* pSteps ); + void DeleteSteps( const Steps* pSteps, bool bReAutoGen = true ); + + void FreeAllLoadedFromProfile( ProfileSlot slot = ProfileSlot_Invalid, const set *setInUse = NULL ); + bool WasLoadedFromProfile() const { return m_LoadedFromProfile != ProfileSlot_Invalid; } + void GetStepsLoadedFromProfile( ProfileSlot slot, vector &vpStepsOut ) const; + int GetNumStepsLoadedFromProfile( ProfileSlot slot ) const; + bool IsEditAlreadyLoaded( Steps* pSteps ) const; + + bool IsStepsUsingDifferentTiming(Steps *pSteps ) const; + + /** + * @brief An array of keysound file names (e.g. "beep.wav"). + * The index in this array corresponds to the index in TapNote. + * If you change the index in here, you must change all NoteData too. + * Any note that doesn't have a value in the range of this array + * means "this note doesn't have a keysound". */ + vector m_vsKeysoundFile; + + CachedObject m_CachedObject; + + RString GetAttackString() const + { + return join(":", this->m_sAttackString); + } + + // Lua + void PushSelf( lua_State *L ); + +private: + /** @brief the Steps that belong to this Song. */ + vector m_vpSteps; + /** @brief the Steps of a particular StepsType that belong to this Song. */ + vector m_vpStepsByType[NUM_StepsType]; +}; + +#endif + +/** + * @file + * @author Chris Danford, Glenn Maynard (c) 2001-2004 + * @section LICENSE + * All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, and/or sell copies of the Software, and to permit persons to + * whom the Software is furnished to do so, provided that the above + * copyright notice(s) and this permission notice appear in all copies of + * the Software and that both the above copyright notice(s) and this + * permission notice appear in supporting documentation. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS + * INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT + * OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS + * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ diff --git a/src/SongManager.cpp b/src/SongManager.cpp index 398dfff8d3..c38f9d039e 100644 --- a/src/SongManager.cpp +++ b/src/SongManager.cpp @@ -1,2112 +1,2130 @@ -#include "global.h" -#include "SongManager.h" -#include "arch/LoadingWindow/LoadingWindow.h" -#include "AnnouncerManager.h" -#include "BackgroundUtil.h" -#include "BannerCache.h" -#include "CommonMetrics.h" -#include "Course.h" -#include "CourseLoaderCRS.h" -#include "CourseUtil.h" -#include "Foreach.h" -#include "GameManager.h" -#include "GameState.h" -#include "LocalizedString.h" -#include "MsdFile.h" -#include "NoteSkinManager.h" -#include "NotesLoaderDWI.h" -#include "NotesLoaderSSC.h" -#include "NotesLoaderSM.h" -#include "PrefsManager.h" -#include "Profile.h" -#include "ProfileManager.h" -#include "RageFile.h" -#include "RageFileManager.h" -#include "RageLog.h" -#include "Song.h" -#include "SongUtil.h" -#include "Sprite.h" -#include "StatsManager.h" -#include "Steps.h" -#include "StepsUtil.h" -#include "Style.h" -#include "ThemeManager.h" -#include "TitleSubstitution.h" -#include "TrailUtil.h" -#include "UnlockManager.h" -#include "SpecialFiles.h" - -SongManager* SONGMAN = NULL; // global and accessable from anywhere in our program - -const RString ADDITIONAL_SONGS_DIR = "/AdditionalSongs/"; -const RString ADDITIONAL_COURSES_DIR = "/AdditionalCourses/"; -const RString EDIT_SUBDIR = "Edits/"; - -/** @brief The file that contains various random attacks. */ -const RString ATTACK_FILE = "/Data/RandomAttacks.txt"; - -static const ThemeMetric EXTRA_COLOR ( "SongManager", "ExtraColor" ); -static const ThemeMetric EXTRA_COLOR_METER ( "SongManager", "ExtraColorMeter" ); -static const ThemeMetric USE_PREFERRED_SORT_COLOR ( "SongManager", "UsePreferredSortColor" ); -static const ThemeMetric USE_UNLOCK_COLOR ( "SongManager", "UseUnlockColor" ); -static const ThemeMetric UNLOCK_COLOR ( "SongManager", "UnlockColor" ); -static const ThemeMetric MOVE_UNLOCKS_TO_BOTTOM_OF_PREFERRED_SORT ( "SongManager", "MoveUnlocksToBottomOfPreferredSort" ); -static const ThemeMetric EXTRA_STAGE2_DIFFICULTY_MAX ( "SongManager", "ExtraStage2DifficultyMax" ); - -static Preference g_sDisabledSongs( "DisabledSongs", "" ); -static Preference g_bHideIncompleteCourses( "HideIncompleteCourses", false ); - -RString SONG_GROUP_COLOR_NAME( size_t i ) { return ssprintf( "SongGroupColor%i", (int) i+1 ); } -RString COURSE_GROUP_COLOR_NAME( size_t i ) { return ssprintf( "CourseGroupColor%i", (int) i+1 ); } - -SongManager::SongManager() -{ - // Register with Lua. - { - Lua *L = LUA->Get(); - lua_pushstring( L, "SONGMAN" ); - this->PushSelf( L ); - lua_settable( L, LUA_GLOBALSINDEX ); - LUA->Release( L ); - } - - NUM_SONG_GROUP_COLORS .Load( "SongManager", "NumSongGroupColors" ); - SONG_GROUP_COLOR .Load( "SongManager", SONG_GROUP_COLOR_NAME, NUM_SONG_GROUP_COLORS ); - NUM_COURSE_GROUP_COLORS .Load( "SongManager", "NumCourseGroupColors" ); - COURSE_GROUP_COLOR .Load( "SongManager", COURSE_GROUP_COLOR_NAME, NUM_COURSE_GROUP_COLORS ); -} - -SongManager::~SongManager() -{ - // Unregister with Lua. - LUA->UnsetGlobal( "SONGMAN" ); - - // Courses depend on Songs and Songs don't depend on Courses. - // So, delete the Courses first. - FreeCourses(); - FreeSongs(); -} - -void SongManager::InitAll( LoadingWindow *ld ) -{ - InitSongsFromDisk( ld ); - InitCoursesFromDisk( ld ); - InitAutogenCourses(); - InitRandomAttacks(); -} - -static LocalizedString RELOADING ( "SongManager", "Reloading..." ); -static LocalizedString UNLOADING_SONGS ( "SongManager", "Unloading songs..." ); -static LocalizedString UNLOADING_COURSES ( "SongManager", "Unloading courses..." ); - -void SongManager::Reload( bool bAllowFastLoad, LoadingWindow *ld ) -{ - FILEMAN->FlushDirCache( SpecialFiles::SONGS_DIR ); - FILEMAN->FlushDirCache( ADDITIONAL_SONGS_DIR ); - FILEMAN->FlushDirCache( SpecialFiles::COURSES_DIR ); - FILEMAN->FlushDirCache( ADDITIONAL_COURSES_DIR ); - FILEMAN->FlushDirCache( EDIT_SUBDIR ); - - if( ld ) - ld->SetText( RELOADING ); - - // save scores before unloading songs, or the scores will be lost - PROFILEMAN->SaveMachineProfile(); - - if( ld ) - ld->SetText( UNLOADING_COURSES ); - - FreeCourses(); - - if( ld ) - ld->SetText( UNLOADING_SONGS ); - - FreeSongs(); - - const bool OldVal = PREFSMAN->m_bFastLoad; - if( !bAllowFastLoad ) - PREFSMAN->m_bFastLoad.Set( false ); - - InitAll( ld ); - - // reload scores and unlocks afterward - PROFILEMAN->LoadMachineProfile(); - UNLOCKMAN->Reload(); - - if( !bAllowFastLoad ) - PREFSMAN->m_bFastLoad.Set( OldVal ); - - UpdatePreferredSort(); -} - -void SongManager::InitSongsFromDisk( LoadingWindow *ld ) -{ - RageTimer tm; - LoadStepManiaSongDir( SpecialFiles::SONGS_DIR, ld ); - - const bool bOldVal = PREFSMAN->m_bFastLoad; - PREFSMAN->m_bFastLoad.Set( PREFSMAN->m_bFastLoadAdditionalSongs ); - LoadStepManiaSongDir( ADDITIONAL_SONGS_DIR, ld ); - PREFSMAN->m_bFastLoad.Set( bOldVal ); - - LOG->Trace( "Found %d songs in %f seconds.", (int)m_pSongs.size(), tm.GetDeltaTime() ); -} - -static LocalizedString FOLDER_CONTAINS_MUSIC_FILES( "SongManager", "The folder \"%s\" appears to be a song folder. All song folders must reside in a group folder. For example, \"Songs/Originals/My Song\"." ); -void SongManager::SanityCheckGroupDir( RString sDir ) const -{ - // Check to see if they put a song directly inside the group folder. - vector arrayFiles; - GetDirListing( sDir + "/*.mp3", arrayFiles ); - GetDirListing( sDir + "/*.oga", arrayFiles ); - GetDirListing( sDir + "/*.ogg", arrayFiles ); - GetDirListing( sDir + "/*.wav", arrayFiles ); - if( !arrayFiles.empty() ) - RageException::Throw( FOLDER_CONTAINS_MUSIC_FILES.GetValue(), sDir.c_str() ); -} - -void SongManager::AddGroup( RString sDir, RString sGroupDirName ) -{ - unsigned j; - for(j = 0; j < m_sSongGroupNames.size(); ++j) - if( sGroupDirName == m_sSongGroupNames[j] ) - break; - - if( j != m_sSongGroupNames.size() ) - return; // the group is already added - - // Look for a group banner in this group folder - vector arrayGroupBanners; - GetDirListing( sDir+sGroupDirName+"/*.png", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+"/*.jpg", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+"/*.jpeg", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+"/*.gif", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+"/*.bmp", arrayGroupBanners ); - - RString sBannerPath; - if( !arrayGroupBanners.empty() ) - sBannerPath = sDir+sGroupDirName+"/"+arrayGroupBanners[0] ; - else - { - // Look for a group banner in the parent folder - GetDirListing( sDir+sGroupDirName+".png", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+".jpg", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+".jpeg", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+".gif", arrayGroupBanners ); - GetDirListing( sDir+sGroupDirName+".bmp", arrayGroupBanners ); - if( !arrayGroupBanners.empty() ) - sBannerPath = sDir+arrayGroupBanners[0]; - } - - /* Other group graphics are a bit trickier, and usually don't exist. - * A themer has a few options, namely checking the aspect ratio and - * operating on it. -aj - * TODO: Once the files are implemented in Song, bring the extensions - * from there into here. -aj */ - // Group background - - //vector arrayGroupBackgrounds; - //GetDirListing( sDir+sGroupDirName+"/*-bg.png", arrayGroupBanners ); - //GetDirListing( sDir+sGroupDirName+"/*-bg.jpg", arrayGroupBanners ); - //GetDirListing( sDir+sGroupDirName+"/*-bg.jpeg", arrayGroupBanners ); - //GetDirListing( sDir+sGroupDirName+"/*-bg.gif", arrayGroupBanners ); - //GetDirListing( sDir+sGroupDirName+"/*-bg.bmp", arrayGroupBanners ); -/* - RString sBackgroundPath; - if( !arrayGroupBackgrounds.empty() ) - sBackgroundPath = sDir+sGroupDirName+"/"+arrayGroupBackgrounds[0]; - else - { - // Look for a group background in the parent folder - GetDirListing( sDir+sGroupDirName+"-bg.png", arrayGroupBackgrounds ); - GetDirListing( sDir+sGroupDirName+"-bg.jpg", arrayGroupBackgrounds ); - GetDirListing( sDir+sGroupDirName+"-bg.jpeg", arrayGroupBackgrounds ); - GetDirListing( sDir+sGroupDirName+"-bg.gif", arrayGroupBackgrounds ); - GetDirListing( sDir+sGroupDirName+"-bg.bmp", arrayGroupBackgrounds ); - if( !arrayGroupBackgrounds.empty() ) - sBackgroundPath = sDir+arrayGroupBackgrounds[0]; - } -*/ - /* - LOG->Trace( "Group banner for '%s' is '%s'.", sGroupDirName.c_str(), - sBannerPath != ""? sBannerPath.c_str():"(none)" ); - */ - m_sSongGroupNames.push_back( sGroupDirName ); - m_sSongGroupBannerPaths.push_back( sBannerPath ); - //m_sSongGroupBackgroundPaths.push_back( sBackgroundPath ); -} - -static LocalizedString LOADING_SONGS ( "SongManager", "Loading songs..." ); -void SongManager::LoadStepManiaSongDir( RString sDir, LoadingWindow *ld ) -{ - // Make sure sDir has a trailing slash. - if( sDir.Right(1) != "/" ) - sDir += "/"; - - // Find all group directories in "Songs" folder - vector arrayGroupDirs; - GetDirListing( sDir+"*", arrayGroupDirs, true ); - SortRStringArray( arrayGroupDirs ); - StripCvsAndSvn( arrayGroupDirs ); - StripMacResourceForks( arrayGroupDirs ); - - vector< vector > arrayGroupSongDirs; - int groupIndex, songCount, songIndex; - - groupIndex = 0; - songCount = 0; - FOREACH_CONST( RString, arrayGroupDirs, s ) // foreach dir in /Songs/ - { - RString sGroupDirName = *s; - // TODO: If this check fails, log a warning instead of crashing. - SanityCheckGroupDir(sDir+sGroupDirName); - - // Find all Song folders in this group directory - vector arraySongDirs; - GetDirListing( sDir+sGroupDirName + "/*", arraySongDirs, true, true ); - StripCvsAndSvn( arraySongDirs ); - StripMacResourceForks( arraySongDirs ); - SortRStringArray( arraySongDirs ); - - arrayGroupSongDirs.push_back(arraySongDirs); - songCount += arraySongDirs.size(); - - } - - if( songCount==0 ) return; - - if( ld ) { - ld->SetIndeterminate( false ); - ld->SetTotalWork( songCount ); - } - - groupIndex = 0; - songIndex = 0; - FOREACH_CONST( RString, arrayGroupDirs, s ) // foreach dir in /Songs/ - { - RString sGroupDirName = *s; - vector &arraySongDirs = arrayGroupSongDirs[groupIndex++]; - - LOG->Trace("Attempting to load %i songs from \"%s\"", int(arraySongDirs.size()), - (sDir+sGroupDirName).c_str() ); - int loaded = 0; - - SongPointerVector& index_entry = m_mapSongGroupIndex[sGroupDirName]; - - for( unsigned j=0; j< arraySongDirs.size(); ++j ) // for each song dir - { - RString sSongDirName = arraySongDirs[j]; - - // this is a song directory. Load a new song. - if( ld ) - { - ld->SetProgress(songIndex); - ld->SetText( LOADING_SONGS.GetValue() + - ssprintf("\n%s\n%s", - Basename(sGroupDirName).c_str(), - Basename(sSongDirName).c_str() - ) - ); - } - Song* pNewSong = new Song; - m_pSongs.push_back( pNewSong ); - if( !pNewSong->LoadFromSongDir( sSongDirName ) ) - { - // The song failed to load. - m_pSongs.pop_back(); - delete pNewSong; - continue; - } - - index_entry.push_back( pNewSong ); - loaded++; - songIndex++; - } - - LOG->Trace("Loaded %i songs from \"%s\"", loaded, (sDir+sGroupDirName).c_str() ); - - // Don't add the group name if we didn't load any songs in this group. - if(!loaded) continue; - - // Add this group to the group array. - AddGroup(sDir, sGroupDirName); - - // Cache and load the group banner. (and background if it has one -aj) - BANNERCACHE->CacheBanner( GetSongGroupBannerPath(sGroupDirName) ); - - // Load the group sym links (if any) - LoadGroupSymLinks(sDir, sGroupDirName); - } - - if( ld ) { - ld->SetIndeterminate( true ); - } - - LoadEnabledSongsFromPref(); -} - -// Instead of "symlinks", songs should have membership in multiple groups. -Chris -void SongManager::LoadGroupSymLinks(RString sDir, RString sGroupFolder) -{ - // Find all symlink files in this folder - vector arraySymLinks; - GetDirListing( sDir+sGroupFolder+"/*.include", arraySymLinks, false ); - SortRStringArray( arraySymLinks ); - SongPointerVector& index_entry = m_mapSongGroupIndex[sGroupFolder]; - for( unsigned s=0; s< arraySymLinks.size(); s++ ) // for each symlink in this dir, add it in as a song. - { - MsdFile msdF; - msdF.ReadFile( sDir+sGroupFolder+"/"+arraySymLinks[s].c_str(), false ); // don't unescape - RString sSymDestination = msdF.GetParam(0,1); // Should only be 1 value & param...period. - - Song* pNewSong = new Song; - if( !pNewSong->LoadFromSongDir( sSymDestination ) ) - { - delete pNewSong; // The song failed to load. - } - else - { - const vector& vpSteps = pNewSong->GetAllSteps(); - while( vpSteps.size() ) - pNewSong->DeleteSteps( vpSteps[0] ); - - FOREACH_BackgroundLayer( i ) - pNewSong->GetBackgroundChanges(i).clear(); - - pNewSong->m_bIsSymLink = true; // Very important so we don't double-parse later - pNewSong->m_sGroupName = sGroupFolder; - m_pSongs.push_back( pNewSong ); - index_entry.push_back( pNewSong ); - } - } -} - -void SongManager::PreloadSongImages() -{ - if( PREFSMAN->m_BannerCache != BNCACHE_FULL ) - return; - - /* Load textures before unloading old ones, so we don't reload textures - * that we don't need to. */ - RageTexturePreloader preload; - - const vector &songs = GetAllSongs(); - for( unsigned i = 0; i < songs.size(); ++i ) - { - if( !songs[i]->HasBanner() ) - continue; - - const RageTextureID ID = Sprite::SongBannerTexture( songs[i]->GetBannerPath() ); - preload.Load( ID ); - } - - vector courses; - GetAllCourses( courses, false ); - for( unsigned i = 0; i < courses.size(); ++i ) - { - if( !courses[i]->HasBanner() ) - continue; - - const RageTextureID ID = Sprite::SongBannerTexture( courses[i]->GetBannerPath() ); - preload.Load( ID ); - } - - preload.Swap( m_TexturePreload ); -} - -void SongManager::FreeSongs() -{ - m_sSongGroupNames.clear(); - m_sSongGroupBannerPaths.clear(); - //m_sSongGroupBackgroundPaths.clear(); - - for( unsigned i=0; i &AddTo ) const -{ - AddTo.insert(AddTo.end(), m_sSongGroupNames.begin(), m_sSongGroupNames.end() ); -} - -bool SongManager::DoesSongGroupExist( RString sSongGroup ) const -{ - return find( m_sSongGroupNames.begin(), m_sSongGroupNames.end(), sSongGroup ) != m_sSongGroupNames.end(); -} - -RageColor SongManager::GetSongGroupColor( const RString &sSongGroup ) const -{ - for( unsigned i=0; iFindSong( pSong ); - if( pUE && USE_UNLOCK_COLOR.GetValue() ) - return UNLOCK_COLOR.GetValue(); - - if( USE_PREFERRED_SORT_COLOR ) - { - FOREACH_CONST( PreferredSortSection, m_vPreferredSongSort, v ) - { - FOREACH_CONST( Song*, v->vpSongs, s ) - { - if( *s == pSong ) - { - int i = v - m_vPreferredSongSort.begin(); - return SONG_GROUP_COLOR.GetValue( i%NUM_SONG_GROUP_COLORS ); - } - } - } - - int i = m_vPreferredSongSort.size(); - return SONG_GROUP_COLOR.GetValue( i%NUM_SONG_GROUP_COLORS ); - } - else // TODO: Have a better fallback plan with colors? - { - /* XXX: Previously, this matched all notes, which set a song to "extra" - * if it had any 10-foot steps at all, even edits or doubles. - * - * For now, only look at notes for the current note type. This means - * that if a song has 10-foot steps on Doubles, it'll only show up red - * in Doubles. That's not too bad, I think. This will also change it - * in the song scroll, which is a little odd but harmless. - * - * XXX: Ack. This means this function can only be called when we have - * a style set up, which is too restrictive. How to handle this? */ - //const StepsType st = GAMESTATE->GetCurrentStyle()->m_StepsType; - const vector& vpSteps = pSong->GetAllSteps(); - for( unsigned i=0; iGetDifficulty() ) - { - case Difficulty_Challenge: - case Difficulty_Edit: - continue; - default: break; - } - - //if(pSteps->m_StepsType != st) - // continue; - - if( pSteps->GetMeter() >= EXTRA_COLOR_METER ) - return (RageColor)EXTRA_COLOR; - } - if( pSong->m_sMainTitle == "DVNO") // XXX: What IS this? An easter egg? -Wolfman2000 - { - return RageColor(1.0f,0.8f,0.0f,1.0f); - } - else - { - return GetSongGroupColor( pSong->m_sGroupName ); - } - } -} - -RString SongManager::GetCourseGroupBannerPath( const RString &sCourseGroup ) const -{ - map::const_iterator iter = m_mapCourseGroupToInfo.find( sCourseGroup ); - if( iter == m_mapCourseGroupToInfo.end() ) - { - ASSERT_M( 0, ssprintf("requested banner for course group '%s' that doesn't exist",sCourseGroup.c_str()) ); - return RString(); - } - else - { - return iter->second.m_sBannerPath; - } -} - -void SongManager::GetCourseGroupNames( vector &AddTo ) const -{ - FOREACHM_CONST( RString, CourseGroupInfo, m_mapCourseGroupToInfo, iter ) - AddTo.push_back( iter->first ); -} - -bool SongManager::DoesCourseGroupExist( const RString &sCourseGroup ) const -{ - return m_mapCourseGroupToInfo.find( sCourseGroup ) != m_mapCourseGroupToInfo.end(); -} - -RageColor SongManager::GetCourseGroupColor( const RString &sCourseGroup ) const -{ - int iIndex = 0; - FOREACHM_CONST( RString, CourseGroupInfo, m_mapCourseGroupToInfo, iter ) - { - if( iter->first == sCourseGroup ) - return SONG_GROUP_COLOR.GetValue( iIndex%NUM_SONG_GROUP_COLORS ); - iIndex++; - } - - ASSERT_M( 0, ssprintf("requested color for course group '%s' that doesn't exist",sCourseGroup.c_str()) ); - return RageColor(1,1,1,1); -} - -RageColor SongManager::GetCourseColor( const Course* pCourse ) const -{ - // Use unlock color if applicable - const UnlockEntry *pUE = UNLOCKMAN->FindCourse( pCourse ); - if( pUE && USE_UNLOCK_COLOR.GetValue() ) - return UNLOCK_COLOR.GetValue(); - - if( USE_PREFERRED_SORT_COLOR ) - { - FOREACH_CONST( CoursePointerVector, m_vPreferredCourseSort, v ) - { - FOREACH_CONST( Course*, *v, s ) - { - if( *s == pCourse ) - { - int i = v - m_vPreferredCourseSort.begin(); - CHECKPOINT_M( ssprintf( "%i, NUM_COURSE_GROUP_COLORS = %i", i, NUM_COURSE_GROUP_COLORS.GetValue()) ); - return COURSE_GROUP_COLOR.GetValue( i % NUM_COURSE_GROUP_COLORS ); - } - } - } - - int i = m_vPreferredCourseSort.size(); - CHECKPOINT_M( ssprintf( "%i, NUM_COURSE_GROUP_COLORS = %i", i, NUM_COURSE_GROUP_COLORS.GetValue()) ); - return COURSE_GROUP_COLOR.GetValue( i % NUM_COURSE_GROUP_COLORS ); - } - else - { - return GetCourseGroupColor( pCourse->m_sGroupName ); - } -} - -void SongManager::ResetGroupColors() -{ - // Reload song/course group colors to prevent a crash when switching - // themes in-game. (apparently not, though.) -aj - SONG_GROUP_COLOR.Clear(); - COURSE_GROUP_COLOR.Clear(); - - NUM_SONG_GROUP_COLORS .Load( "SongManager", "NumSongGroupColors" ); - SONG_GROUP_COLOR .Load( "SongManager", SONG_GROUP_COLOR_NAME, NUM_SONG_GROUP_COLORS ); - NUM_COURSE_GROUP_COLORS .Load( "SongManager", "NumCourseGroupColors" ); - COURSE_GROUP_COLOR .Load( "SongManager", COURSE_GROUP_COLOR_NAME, NUM_COURSE_GROUP_COLORS ); -} - -const vector &SongManager::GetSongs( const RString &sGroupName ) const -{ - static const vector vEmpty; - - if( sGroupName == GROUP_ALL ) - return m_pSongs; - map::const_iterator iter = m_mapSongGroupIndex.find( sGroupName ); - if ( iter != m_mapSongGroupIndex.end() ) - return iter->second; - return vEmpty; -} - -void SongManager::GetPreferredSortSongs( vector &AddTo ) const -{ - if( m_vPreferredSongSort.empty() ) - { - AddTo.insert( AddTo.end(), m_pSongs.begin(), m_pSongs.end() ); - return; - } - - FOREACH_CONST( PreferredSortSection, m_vPreferredSongSort, v ) - AddTo.insert( AddTo.end(), v->vpSongs.begin(), v->vpSongs.end() ); -} - -RString SongManager::SongToPreferredSortSectionName( const Song *pSong ) const -{ - FOREACH_CONST( PreferredSortSection, m_vPreferredSongSort, v ) - { - FOREACH_CONST( Song*, v->vpSongs, s ) - { - if( *s == pSong ) - return v->sName; - } - } - return RString(); -} - -void SongManager::GetPreferredSortCourses( CourseType ct, vector &AddTo, bool bIncludeAutogen ) const -{ - if( m_vPreferredCourseSort.empty() ) - { - GetCourses( ct, AddTo, bIncludeAutogen ); - return; - } - - FOREACH_CONST( CoursePointerVector, m_vPreferredCourseSort, v ) - { - FOREACH_CONST( Course*, *v, c ) - { - Course *pCourse = *c; - if( pCourse->GetCourseType() == ct ) - AddTo.push_back( pCourse ); - } - } -} - -int SongManager::GetNumSongs() const -{ - return m_pSongs.size(); -} - -int SongManager::GetNumLockedSongs() const -{ - int iNum = 0; - FOREACH_CONST( Song*, m_pSongs, i ) - { - // If locked for any reason, regardless of how it's locked. - if( UNLOCKMAN->SongIsLocked(*i) ) - ++iNum; - } - return iNum; -} - -int SongManager::GetNumUnlockedSongs() const -{ - int iNum = 0; - FOREACH_CONST( Song*, m_pSongs, i ) - { - // If locked for any reason other than LOCKED_LOCK: - if( UNLOCKMAN->SongIsLocked(*i) & ~LOCKED_LOCK ) - continue; - ++iNum; - } - return iNum; -} - -int SongManager::GetNumSelectableAndUnlockedSongs() const -{ - int iNum = 0; - FOREACH_CONST( Song*, m_pSongs, i ) - { - // If locked for any reason other than LOCKED_LOCK or LOCKED_SELECTABLE: - if( UNLOCKMAN->SongIsLocked(*i) & ~(LOCKED_LOCK|LOCKED_SELECTABLE) ) - continue; - ++iNum; - } - return iNum; -} - -int SongManager::GetNumAdditionalSongs() const -{ - int iNum = 0; - FOREACH_CONST( Song*, m_pSongs, i ) - { - if( WasLoadedFromAdditionalSongs( *i ) ) - ++iNum; - } - return iNum; -} - -int SongManager::GetNumSongGroups() const -{ - return m_sSongGroupNames.size(); -} - -int SongManager::GetNumCourses() const -{ - return m_pCourses.size(); -} - -int SongManager::GetNumAdditionalCourses() const -{ - int iNum = 0; - FOREACH_CONST( Course*, m_pCourses, i ) - { - if( WasLoadedFromAdditionalCourses( *i ) ) - ++iNum; - } - return iNum; -} - -int SongManager::GetNumCourseGroups() const -{ - return m_mapCourseGroupToInfo.size(); -} - -RString SongManager::ShortenGroupName( RString sLongGroupName ) -{ - static TitleSubst tsub("Groups"); - - TitleFields title; - title.Title = sLongGroupName; - tsub.Subst( title ); - return title.Title; -} - -static LocalizedString LOADING_COURSES ( "SongManager", "Loading courses..." ); -void SongManager::InitCoursesFromDisk( LoadingWindow *ld ) -{ - LOG->Trace( "Loading courses." ); - - vector vsCourseDirs; - vsCourseDirs.push_back( SpecialFiles::COURSES_DIR ); - vsCourseDirs.push_back( ADDITIONAL_COURSES_DIR ); - - vector vsCourseGroupNames; - FOREACH_CONST( RString, vsCourseDirs, sDir ) - { - // Find all group directories in Courses dir - GetDirListing( *sDir + "*", vsCourseGroupNames, true, true ); - StripCvsAndSvn( vsCourseGroupNames ); - StripMacResourceForks( vsCourseGroupNames ); - } - - // Search for courses both in COURSES_DIR and in subdirectories - vsCourseGroupNames.push_back( SpecialFiles::COURSES_DIR ); - SortRStringArray( vsCourseGroupNames ); - - int courseIndex = 0; - FOREACH_CONST( RString, vsCourseGroupNames, sCourseGroup ) // for each dir in /Courses/ - { - // Find all CRS files in this group directory - vector vsCoursePaths; - GetDirListing( *sCourseGroup + "/*.crs", vsCoursePaths, false, true ); - SortRStringArray( vsCoursePaths ); - - if( ld ) - { - ld->SetIndeterminate( false ); - ld->SetTotalWork( vsCoursePaths.size() ); - } - - FOREACH_CONST( RString, vsCoursePaths, sCoursePath ) - { - if( ld ) - { - ld->SetProgress(courseIndex); - ld->SetText( LOADING_COURSES.GetValue()+ssprintf("\n%s\n%s", - Basename(*sCourseGroup).c_str(), - Basename(*sCoursePath).c_str())); - } - - Course* pCourse = new Course; - CourseLoaderCRS::LoadFromCRSFile( *sCoursePath, *pCourse ); - - if( g_bHideIncompleteCourses.Get() && pCourse->m_bIncomplete ) - { - delete pCourse; - continue; - } - - m_pCourses.push_back( pCourse ); - courseIndex++; - } - } - - if( ld ) { - ld->SetIndeterminate( true ); - } - - RefreshCourseGroupInfo(); -} - -void SongManager::InitAutogenCourses() -{ - // Create group courses for Endless and Nonstop - vector saGroupNames; - this->GetSongGroupNames( saGroupNames ); - Course* pCourse; - for( unsigned g=0; gm_sScripter = "Autogen"; - m_pCourses.push_back( pCourse ); - - pCourse = new Course; - CourseUtil::AutogenNonstopFromGroup( sGroupName, Difficulty_Medium, *pCourse ); - pCourse->m_sScripter = "Autogen"; - m_pCourses.push_back( pCourse ); - } - - vector apCourseSongs = GetAllSongs(); - - // Generate "All Songs" endless course. - pCourse = new Course; - CourseUtil::AutogenEndlessFromGroup( "", Difficulty_Medium, *pCourse ); - pCourse->m_sScripter = "Autogen"; - m_pCourses.push_back( pCourse ); - - /* Generate Oni courses from artists. Only create courses if we have at least - * four songs from an artist; create 3- and 4-song courses. */ - { - /* We normally sort by translit artist. However, display artist is more - * consistent. For example, transliterated Japanese names are alternately - * spelled given- and family-name first, but display titles are more consistent. */ - vector apSongs = this->GetAllSongs(); - SongUtil::SortSongPointerArrayByDisplayArtist( apSongs ); - - RString sCurArtist = ""; - RString sCurArtistTranslit = ""; - int iCurArtistCount = 0; - - vector aSongs; - unsigned i = 0; - do { - RString sArtist = i >= apSongs.size()? RString(""): apSongs[i]->GetDisplayArtist(); - RString sTranslitArtist = i >= apSongs.size()? RString(""): apSongs[i]->GetTranslitArtist(); - if( i < apSongs.size() && !sCurArtist.CompareNoCase(sArtist) ) - { - aSongs.push_back( apSongs[i] ); - ++iCurArtistCount; - continue; - } - - /* Different artist, or we're at the end. If we have enough entries for - * the last artist, add it. Skip blanks and "Unknown artist". */ - if( iCurArtistCount >= 3 && sCurArtistTranslit != "" && - sCurArtistTranslit.CompareNoCase("Unknown artist") && - sCurArtist.CompareNoCase("Unknown artist") ) - { - pCourse = new Course; - CourseUtil::AutogenOniFromArtist( sCurArtist, sCurArtistTranslit, aSongs, Difficulty_Hard, *pCourse ); - pCourse->m_sScripter = "Autogen"; - m_pCourses.push_back( pCourse ); - } - - aSongs.clear(); - - if( i < apSongs.size() ) - { - sCurArtist = sArtist; - sCurArtistTranslit = sTranslitArtist; - iCurArtistCount = 1; - aSongs.push_back( apSongs[i] ); - } - } while( i++ < apSongs.size() ); - } -} - -void SongManager::InitRandomAttacks() -{ - GAMESTATE->m_RandomAttacks.clear(); - - if( !IsAFile(ATTACK_FILE) ) - LOG->Trace( "File Data/RandomAttacks.txt was not found" ); - else - { - MsdFile msd; - - if( !msd.ReadFile( ATTACK_FILE, true ) ) - LOG->Warn( "Error opening file '%s' for reading: %s.", ATTACK_FILE.c_str(), msd.GetError().c_str() ); - else - { - for( unsigned i=0; i 2 ) - { - LOG->Warn( "Got \"%s:%s\" tag with too many parameters", sType.c_str(), sAttack.c_str() ); - continue; - } - - if( !sType.EqualsNoCase("ATTACK") ) - { - LOG->Warn( "Got \"%s:%s\" tag with wrong declaration", sType.c_str(), sAttack.c_str() ); - continue; - } - - GAMESTATE->m_RandomAttacks.push_back( sAttack ); - } - } - } -} - -void SongManager::FreeCourses() -{ - for( unsigned i=0; i vNewCourses; - for( vector::iterator it = m_pCourses.begin(); it != m_pCourses.end(); ++it ) - { - if( (*it)->m_bIsAutogen ) - { - delete *it; - } - else - { - vNewCourses.push_back( *it ); - } - } - m_pCourses.swap( vNewCourses ); - UpdatePopular(); - UpdateShuffled(); - RefreshCourseGroupInfo(); -} - -void SongManager::AddCourse( Course *pCourse ) -{ - m_pCourses.push_back( pCourse ); - UpdatePopular(); - UpdateShuffled(); - m_mapCourseGroupToInfo[ pCourse->m_sGroupName ]; // insert -} - -void SongManager::DeleteCourse( Course *pCourse ) -{ - vector::iterator iter = find( m_pCourses.begin(), m_pCourses.end(), pCourse ); - ASSERT( iter != m_pCourses.end() ); - m_pCourses.erase( iter ); - UpdatePopular(); - UpdateShuffled(); - RefreshCourseGroupInfo(); -} - -void SongManager::InvalidateCachedTrails() -{ - FOREACH_CONST( Course *, m_pCourses, pCourse ) - { - const Course &c = **pCourse; - - if( c.IsAnEdit() ) - c.m_TrailCache.clear(); - } -} - -/* Called periodically to wipe out cached NoteData. This is called when we - * change screens. */ -void SongManager::Cleanup() -{ - for( unsigned i=0; i& vpSteps = pSong->GetAllSteps(); - for( unsigned n=0; nCompress(); - } - } - } -} - -/* Flush all Song*, Steps* and Course* caches. This is when a Song or its Steps - * are removed or changed. This doesn't touch GAMESTATE and StageStats - * pointers. Currently, the only time Steps are altered independently of the - * Courses and Songs is in Edit Mode, which updates the other pointers it needs. */ -void SongManager::Invalidate( const Song *pStaleSong ) -{ - // TODO: This is unnecessarily expensive. - // Can we regenerate only the autogen courses that are affected? - DeleteAutogenCourses(); - - FOREACH( Course*, this->m_pCourses, pCourse ) - { - (*pCourse)->Invalidate( pStaleSong ); - } - - InitAutogenCourses(); - - UpdatePopular(); - UpdateShuffled(); - RefreshCourseGroupInfo(); -} - -void SongManager::RegenerateNonFixedCourses() -{ - for( unsigned i=0; i < m_pCourses.size(); i++ ) - m_pCourses[i]->RegenerateNonFixedTrails(); -} - -void SongManager::SetPreferences() -{ - for( unsigned int i=0; im_bAutogenSteps may have changed. - m_pSongs[i]->RemoveAutoGenNotes(); - m_pSongs[i]->AddAutoGenNotes(); - } -} - -void SongManager::SaveEnabledSongsToPref() -{ - vector vsDisabledSongs; - - // Intentionally drop disabled song entries for songs that aren't currently loaded. - - const vector &apSongs = SONGMAN->GetAllSongs(); - FOREACH_CONST( Song *, apSongs, s ) - { - Song *pSong = (*s); - SongID sid; - sid.FromSong( pSong ); - if( !pSong->GetEnabled() ) - vsDisabledSongs.push_back( sid.ToString() ); - } - g_sDisabledSongs.Set( join(";", vsDisabledSongs) ); -} - -void SongManager::LoadEnabledSongsFromPref() -{ - FOREACH( Song *, m_pSongs, s ) - (*s)->SetEnabled( true ); - - vector asDisabledSongs; - split( g_sDisabledSongs, ";", asDisabledSongs, true ); - - FOREACH_CONST( RString, asDisabledSongs, s ) - { - SongID sid; - sid.FromString( *s ); - Song *pSong = sid.ToSong(); - if( pSong ) - pSong->SetEnabled( false ); - } -} - -void SongManager::GetStepsLoadedFromProfile( vector &AddTo, ProfileSlot slot ) const -{ - const vector &vSongs = GetAllSongs(); - FOREACH_CONST( Song*, vSongs, song ) - { - (*song)->GetStepsLoadedFromProfile( slot, AddTo ); - } -} - -Song *SongManager::GetSongFromSteps( Steps *pSteps ) const -{ - ASSERT( pSteps ); - const vector &vSongs = GetAllSongs(); - FOREACH_CONST( Song*, vSongs, song ) - { - vector vSteps; - SongUtil::GetSteps( *song, vSteps ); - - FOREACH_CONST( Steps*, vSteps, steps ) - { - if( *steps == pSteps ) - { - return *song; - } - } - } - ASSERT(0); - return NULL; -} - -void SongManager::DeleteSteps( Steps *pSteps ) -{ - Song *pSong = GetSongFromSteps( pSteps ); - pSong->DeleteSteps( pSteps ); -} - -bool SongManager::WasLoadedFromAdditionalSongs( const Song *pSong ) const -{ - RString sDir = pSong->GetSongDir(); - return BeginsWith( sDir, ADDITIONAL_SONGS_DIR ); -} - -bool SongManager::WasLoadedFromAdditionalCourses( const Course *pCourse ) const -{ - RString sDir = pCourse->m_sPath; - return BeginsWith( sDir, ADDITIONAL_COURSES_DIR ); -} - -void SongManager::GetAllCourses( vector &AddTo, bool bIncludeAutogen ) const -{ - for( unsigned i=0; im_bIsAutogen ) - AddTo.push_back( m_pCourses[i] ); -} - -void SongManager::GetCourses( CourseType ct, vector &AddTo, bool bIncludeAutogen ) const -{ - for( unsigned i=0; iGetCourseType() == ct ) - if( bIncludeAutogen || !m_pCourses[i]->m_bIsAutogen ) - AddTo.push_back( m_pCourses[i] ); -} - -void SongManager::GetCoursesInGroup( vector &AddTo, const RString &sCourseGroup, bool bIncludeAutogen ) const -{ - for( unsigned i=0; im_sGroupName == sCourseGroup ) - if( bIncludeAutogen || !m_pCourses[i]->m_bIsAutogen ) - AddTo.push_back( m_pCourses[i] ); -} - -bool SongManager::GetExtraStageInfoFromCourse( bool bExtra2, RString sPreferredGroup, Song*& pSongOut, Steps*& pStepsOut ) -{ - const RString sCourseSuffix = sPreferredGroup + (bExtra2 ? "/extra2.crs" : "/extra1.crs"); - RString sCoursePath = SpecialFiles::SONGS_DIR + sCourseSuffix; - - // Couldn't find course in DWI path or alternative song folders - if( !DoesFileExist(sCoursePath) ) - { - sCoursePath = ADDITIONAL_SONGS_DIR + sCourseSuffix; - if( !DoesFileExist(sCoursePath) ) - return false; - } - - Course course; - CourseLoaderCRS::LoadFromCRSFile( sCoursePath, course ); - if( course.GetEstimatedNumStages() <= 0 ) return false; - - Trail *pTrail = course.GetTrail( GAMESTATE->GetCurrentStyle()->m_StepsType ); - if( pTrail->m_vEntries.empty() ) - return false; - - pSongOut = pTrail->m_vEntries[0].pSong; - pStepsOut = pTrail->m_vEntries[0].pSteps; - return true; -} - -// Return true if n1 < n2. -bool CompareNotesPointersForExtra(const Steps *n1, const Steps *n2) -{ - // Equate CHALLENGE to HARD. - Difficulty d1 = min(n1->GetDifficulty(), Difficulty_Hard); - Difficulty d2 = min(n2->GetDifficulty(), Difficulty_Hard); - - if(d1 < d2) return true; - if(d1 > d2) return false; - // n1 difficulty == n2 difficulty - - if(StepsUtil::CompareNotesPointersByMeter(n1,n2)) return true; - if(StepsUtil::CompareNotesPointersByMeter(n2,n1)) return false; - // n1 meter == n2 meter - - return StepsUtil::CompareNotesPointersByRadarValues(n1,n2); -} - -void SongManager::GetExtraStageInfo( bool bExtra2, const Style *sd, Song*& pSongOut, Steps*& pStepsOut ) -{ - RString sGroup = GAMESTATE->m_sPreferredSongGroup; - if( sGroup == GROUP_ALL ) - { - if( GAMESTATE->m_pCurSong == NULL ) - { - // This normally shouldn't happen, but it's helpful to permit it for testing. - LOG->Warn( "GetExtraStageInfo() called in GROUP_ALL, but GAMESTATE->m_pCurSong == NULL" ); - GAMESTATE->m_pCurSong.Set( GetRandomSong() ); - } - sGroup = GAMESTATE->m_pCurSong->m_sGroupName; - } - - ASSERT_M( sGroup != "", ssprintf("%p '%s' '%s'", - GAMESTATE->m_pCurSong.Get(), - GAMESTATE->m_pCurSong? GAMESTATE->m_pCurSong->GetSongDir().c_str():"", - GAMESTATE->m_pCurSong? GAMESTATE->m_pCurSong->m_sGroupName.c_str():"") ); - - // Check preferred group - if( GetExtraStageInfoFromCourse(bExtra2, sGroup, pSongOut, pStepsOut) ) - return; - - // Optionally, check the Songs folder for extra1/2.crs files. - if( GetExtraStageInfoFromCourse(bExtra2, "", pSongOut, pStepsOut) ) - return; - - // Choose a hard song for the extra stage - Song* pExtra1Song = NULL; // the absolute hardest Song and Steps. Use this for extra stage 1. - Steps* pExtra1Notes = NULL; - Song* pExtra2Song = NULL; // a medium-hard Song and Steps. Use this for extra stage 2. - Steps* pExtra2Notes = NULL; - - const vector &apSongs = GetSongs( sGroup ); - for( unsigned s=0; s apSteps; - SongUtil::GetSteps( pSong, apSteps, sd->m_StepsType ); - for( unsigned n=0; n 8 (assuming dance) - if( bExtra2 && pSteps->GetMeter() > EXTRA_STAGE2_DIFFICULTY_MAX ) - continue; // skip - if( pExtra2Notes == NULL || CompareNotesPointersForExtra(pExtra2Notes,pSteps) ) // pSteps is harder than pHardestNotes - { - pExtra2Song = pSong; - pExtra2Notes = pSteps; - } - } - } - - if( pExtra2Song == NULL && pExtra1Song != NULL ) - { - pExtra2Song = pExtra1Song; - pExtra2Notes = pExtra1Notes; - } - - // If there are any notes at all that match this StepsType, everything should be filled out. - // Also, it's guaranteed that there is at least one Steps that matches the StepsType because the player - // had to play something before reaching the extra stage! - ASSERT( pExtra2Song && pExtra1Song && pExtra2Notes && pExtra1Notes ); - - pSongOut = (bExtra2 ? pExtra2Song : pExtra1Song); - pStepsOut = (bExtra2 ? pExtra2Notes : pExtra1Notes); -} - -Song* SongManager::GetRandomSong() -{ - if( m_pShuffledSongs.empty() ) - return NULL; - - static int i = 0; - - for( int iThrowAway=0; iThrowAway<100; iThrowAway++ ) - { - i++; - wrap( i, m_pShuffledSongs.size() ); - Song *pSong = m_pShuffledSongs[ i ]; - if( pSong->IsTutorial() ) - continue; - if( !pSong->NormallyDisplayed() ) - continue; - return pSong; - } - - return NULL; -} - -Course* SongManager::GetRandomCourse() -{ - if( m_pShuffledCourses.empty() ) - return NULL; - - static int i = 0; - - for( int iThrowAway=0; iThrowAway<100; iThrowAway++ ) - { - i++; - wrap( i, m_pShuffledCourses.size() ); - Course *pCourse = m_pShuffledCourses[ i ]; - if( pCourse->m_bIsAutogen && !PREFSMAN->m_bAutogenGroupCourses ) - continue; - if( pCourse->GetCourseType() == COURSE_TYPE_ENDLESS ) - continue; - if( UNLOCKMAN->CourseIsLocked(pCourse) ) - continue; - return pCourse; - } - - return NULL; -} - -Song* SongManager::GetSongFromDir( RString sDir ) const -{ - if( sDir.Right(1) != "/" ) - sDir += "/"; - - sDir.Replace( '\\', '/' ); - - FOREACH_CONST( Song*, m_pSongs, s ) - if( sDir.EqualsNoCase((*s)->GetSongDir()) ) - return *s; - - return NULL; -} - -Course* SongManager::GetCourseFromPath( RString sPath ) const -{ - if( sPath == "" ) - return NULL; - - FOREACH_CONST( Course*, m_pCourses, c ) - { - if( sPath.CompareNoCase((*c)->m_sPath) == 0 ) - return *c; - } - - return NULL; -} - -Course* SongManager::GetCourseFromName( RString sName ) const -{ - if( sName == "" ) - return NULL; - - for( unsigned int i=0; iGetDisplayFullTitle()) == 0 ) - return m_pCourses[i]; - - return NULL; -} - - -/* GetSongDir() contains a path to the song, possibly a full path, eg: - * Songs\Group\SongName or - * My Other Song Folder\Group\SongName or - * c:\Corny J-pop\Group\SongName - * - * Most course group names are "Group\SongName", so we want to match against the - * last two elements. Let's also support "SongName" alone, since the group is - * only important when it's potentially ambiguous. - * - * Let's *not* support "Songs\Group\SongName" in course files. That's probably - * a common error, but that would result in course files floating around that - * only work for people who put songs in "Songs"; we don't want that. */ - -Song *SongManager::FindSong( RString sPath ) const -{ - sPath.Replace( '\\', '/' ); - vector bits; - split( sPath, "/", bits ); - - if( bits.size() == 1 ) - return FindSong( "", bits[0] ); - else if( bits.size() == 2 ) - return FindSong( bits[0], bits[1] ); - - return NULL; -} - -Song *SongManager::FindSong( RString sGroup, RString sSong ) const -{ - // foreach song - const vector &vSongs = GetSongs( sGroup.empty()? GROUP_ALL:sGroup ); - FOREACH_CONST( Song*, vSongs, s ) - { - if( (*s)->Matches(sGroup, sSong) ) - return *s; - } - - return NULL; -} - -Course *SongManager::FindCourse( RString sPath ) const -{ - sPath.Replace( '\\', '/' ); - vector bits; - split( sPath, "/", bits ); - - if( bits.size() == 1 ) - return FindCourse( "", bits[0] ); - else if( bits.size() == 2 ) - return FindCourse( bits[0], bits[1] ); - - return NULL; -} - -Course *SongManager::FindCourse( RString sGroup, RString sName ) const -{ - FOREACH_CONST( Course*, m_pCourses, c ) - { - if( (*c)->Matches(sGroup, sName) ) - return *c; - } - - return NULL; -} - -void SongManager::UpdatePopular() -{ - // update players best - vector apBestSongs = m_pSongs; - for ( unsigned j=0; j < apBestSongs.size() ; ++j ) - { - bool bFiltered = false; - // Filter out locked songs. - if( !apBestSongs[j]->NormallyDisplayed() ) - bFiltered = true; - if( !bFiltered ) - continue; - - // Remove it. - swap( apBestSongs[j], apBestSongs.back() ); - apBestSongs.erase( apBestSongs.end()-1 ); - --j; - } - - SongUtil::SortSongPointerArrayByTitle( apBestSongs ); - - vector apBestCourses[NUM_CourseType]; - FOREACH_ENUM( CourseType, ct ) - { - GetCourses( ct, apBestCourses[ct], PREFSMAN->m_bAutogenGroupCourses ); - CourseUtil::SortCoursePointerArrayByTitle( apBestCourses[ct] ); - } - - m_pPopularSongs = apBestSongs; - SongUtil::SortSongPointerArrayByNumPlays( m_pPopularSongs, ProfileSlot_Machine, true ); - - FOREACH_CourseType( ct ) - { - vector &vpCourses = m_pPopularCourses[ct]; - vpCourses = apBestCourses[ct]; - CourseUtil::SortCoursePointerArrayByNumPlays( vpCourses, ProfileSlot_Machine, true ); - } -} - -void SongManager::UpdateShuffled() -{ - // update shuffled - m_pShuffledSongs = m_pSongs; - random_shuffle( m_pShuffledSongs.begin(), m_pShuffledSongs.end(), g_RandomNumberGenerator ); - - m_pShuffledCourses = m_pCourses; - random_shuffle( m_pShuffledCourses.begin(), m_pShuffledCourses.end(), g_RandomNumberGenerator ); -} - -void SongManager::UpdatePreferredSort(RString sPreferredSongs, RString sPreferredCourses) -{ - ASSERT( UNLOCKMAN ); - - { - m_vPreferredSongSort.clear(); - - vector asLines; - RString sFile = THEME->GetPathO( "SongManager", sPreferredSongs ); - GetFileContents( sFile, asLines ); - if( asLines.empty() ) - return; - - PreferredSortSection section; - map mapSongToPri; - - FOREACH( RString, asLines, s ) - { - RString sLine = *s; - - bool bSectionDivider = BeginsWith(sLine, "---"); - if( bSectionDivider ) - { - if( !section.vpSongs.empty() ) - { - m_vPreferredSongSort.push_back( section ); - section = PreferredSortSection(); - } - - section.sName = sLine.Right( sLine.length() - RString("---").length() ); - TrimLeft( section.sName ); - TrimRight( section.sName ); - } - else - { - Song *pSong = FindSong( sLine ); - if( pSong == NULL ) - continue; - if( UNLOCKMAN->SongIsLocked(pSong) & LOCKED_SELECTABLE ) - continue; - section.vpSongs.push_back( pSong ); - } - } - - if( !section.vpSongs.empty() ) - { - m_vPreferredSongSort.push_back( section ); - section = PreferredSortSection(); - } - - if( MOVE_UNLOCKS_TO_BOTTOM_OF_PREFERRED_SORT.GetValue() ) - { - // move all unlock songs to a group at the bottom - PreferredSortSection PFSection; - PFSection.sName = "Unlocks"; - FOREACH( UnlockEntry, UNLOCKMAN->m_UnlockEntries, ue ) - { - if( ue->m_Type == UnlockRewardType_Song ) - { - Song *pSong = ue->m_Song.ToSong(); - if( pSong ) - PFSection.vpSongs.push_back( pSong ); - } - } - - FOREACH( PreferredSortSection, m_vPreferredSongSort, v ) - { - for( int i=v->vpSongs.size()-1; i>=0; i-- ) - { - Song *pSong = v->vpSongs[i]; - if( find(PFSection.vpSongs.begin(),PFSection.vpSongs.end(),pSong) != PFSection.vpSongs.end() ) - { - v->vpSongs.erase( v->vpSongs.begin()+i ); - } - } - } - - m_vPreferredSongSort.push_back( PFSection ); - } - - // prune empty groups - for( int i=m_vPreferredSongSort.size()-1; i>=0; i-- ) - if( m_vPreferredSongSort[i].vpSongs.empty() ) - m_vPreferredSongSort.erase( m_vPreferredSongSort.begin()+i ); - - FOREACH( PreferredSortSection, m_vPreferredSongSort, i ) - FOREACH( Song*, i->vpSongs, j ) - ASSERT( *j ); - } - - { - m_vPreferredCourseSort.clear(); - - vector asLines; - RString sFile = THEME->GetPathO( "SongManager", sPreferredCourses ); - if( !GetFileContents(sFile, asLines) ) - return; - - vector vpCourses; - - FOREACH( RString, asLines, s ) - { - RString sLine = *s; - bool bSectionDivider = BeginsWith( sLine, "---" ); - if( bSectionDivider ) - { - if( !vpCourses.empty() ) - { - m_vPreferredCourseSort.push_back( vpCourses ); - vpCourses.clear(); - } - continue; - } - - Course *pCourse = FindCourse( sLine ); - if( pCourse == NULL ) - continue; - if( UNLOCKMAN->CourseIsLocked(pCourse) & LOCKED_SELECTABLE ) - continue; - vpCourses.push_back( pCourse ); - } - - if( !vpCourses.empty() ) - { - m_vPreferredCourseSort.push_back( vpCourses ); - vpCourses.clear(); - } - - if( MOVE_UNLOCKS_TO_BOTTOM_OF_PREFERRED_SORT.GetValue() ) - { - // move all unlock Courses to a group at the bottom - vector vpUnlockCourses; - FOREACH( UnlockEntry, UNLOCKMAN->m_UnlockEntries, ue ) - { - if( ue->m_Type == UnlockRewardType_Course ) - if( ue->m_Course.IsValid() ) - vpUnlockCourses.push_back( ue->m_Course.ToCourse() ); - } - - FOREACH( CoursePointerVector, m_vPreferredCourseSort, v ) - { - for( int i=v->size()-1; i>=0; i-- ) - { - Course *pCourse = (*v)[i]; - if( find(vpUnlockCourses.begin(),vpUnlockCourses.end(),pCourse) != vpUnlockCourses.end() ) - { - v->erase( v->begin()+i ); - } - } - } - - m_vPreferredCourseSort.push_back( vpUnlockCourses ); - } - - // prune empty groups - for( int i=m_vPreferredCourseSort.size()-1; i>=0; i-- ) - if( m_vPreferredCourseSort[i].empty() ) - m_vPreferredCourseSort.erase( m_vPreferredCourseSort.begin()+i ); - - FOREACH( CoursePointerVector, m_vPreferredCourseSort, i ) - FOREACH( Course*, *i, j ) - ASSERT( *j ); - } -} - -void SongManager::SortSongs() -{ - SongUtil::SortSongPointerArrayByTitle( m_pSongs ); -} - -void SongManager::UpdateRankingCourses() -{ - /* Updating the ranking courses data is fairly expensive since it involves - * comparing strings. Do so sparingly. */ - vector RankingCourses; - split( THEME->GetMetric("ScreenRanking","CoursesToShow"),",", RankingCourses); - - FOREACH( Course*, m_pCourses, c ) - { - bool bLotsOfStages = (*c)->GetEstimatedNumStages() > 7; - (*c)->m_SortOrder_Ranking = bLotsOfStages? 3 : 2; - - for( unsigned j = 0; j < RankingCourses.size(); j++ ) - if( !RankingCourses[j].CompareNoCase((*c)->m_sPath) ) - (*c)->m_SortOrder_Ranking = 1; - } -} - -void SongManager::RefreshCourseGroupInfo() -{ - m_mapCourseGroupToInfo.clear(); - - FOREACH_CONST( Course*, m_pCourses, c ) - { - m_mapCourseGroupToInfo[(*c)->m_sGroupName]; // insert - } - - // TODO: Search for course group banners - FOREACHM( RString, CourseGroupInfo, m_mapCourseGroupToInfo, iter ) - { - } -} - -void SongManager::LoadStepEditsFromProfileDir( const RString &sProfileDir, ProfileSlot slot ) -{ - { - // Load all edit steps - RString sDir = sProfileDir + EDIT_STEPS_SUBDIR; - - vector vsFiles; - GetDirListing( sDir+"*.edit", vsFiles, false, true ); - - int iNumEditsLoaded = GetNumEditsLoadedFromProfile( slot ); - int size = min( (int) vsFiles.size(), MAX_EDIT_STEPS_PER_PROFILE - iNumEditsLoaded ); - - for( int i=0; i vsFiles; - GetDirListing( sDir+"*.crs", vsFiles, false, true ); - - int iNumEditsLoaded = GetNumEditsLoadedFromProfile( slot ); - int size = min( (int) vsFiles.size(), MAX_EDIT_COURSES_PER_PROFILE - iNumEditsLoaded ); - - for( int i=0; i apSteps; - SongUtil::GetSteps( pSong, apSteps ); - - for( unsigned i = 0; i < apSteps.size(); ++i ) - { - const Steps *pSteps = apSteps[i]; - if( pSteps->WasLoadedFromProfile() && pSteps->GetLoadedFromProfileSlot() == slot ) - ++iCount; - } - } - return iCount; -} - -void SongManager::FreeAllLoadedFromProfile( ProfileSlot slot ) -{ - // Profile courses may refer to profile steps, so free profile courses first. - vector apToDelete; - FOREACH( Course*, m_pCourses, c ) - { - Course *pCourse = *c; - if( pCourse->GetLoadedFromProfileSlot() == ProfileSlot_Invalid ) - continue; - - if( slot == ProfileSlot_Invalid || pCourse->GetLoadedFromProfileSlot() == slot ) - apToDelete.push_back( *c ); - } - - /* We don't use DeleteCourse here, so we don't UpdatePopular and - * UpdateShuffled repeatedly. */ - for( unsigned i = 0; i < apToDelete.size(); ++i ) - { - vector::iterator iter = find( m_pCourses.begin(), m_pCourses.end(), apToDelete[i] ); - ASSERT( iter != m_pCourses.end() ); - m_pCourses.erase( iter ); - delete apToDelete[i]; - } - - // Popular and Shuffled may refer to courses that we just freed. - UpdatePopular(); - UpdateShuffled(); - RefreshCourseGroupInfo(); - - // Free profile steps. - set setInUse; - if( STATSMAN ) - STATSMAN->GetStepsInUse( setInUse ); - FOREACH( Song*, m_pSongs, s ) - (*s)->FreeAllLoadedFromProfile( slot, &setInUse ); -} - -int SongManager::GetNumStepsLoadedFromProfile() -{ - int iCount = 0; - FOREACH( Song*, m_pSongs, s ) - { - vector vpAllSteps = (*s)->GetAllSteps(); - - FOREACH( Steps*, vpAllSteps, ss ) - { - if( (*ss)->GetLoadedFromProfileSlot() != ProfileSlot_Invalid ) - iCount++; - } - } - - return iCount; -} - -template -int FindCourseIndexOfSameMode( T begin, T end, const Course *p ) -{ - const PlayMode pm = p->GetPlayMode(); - - int n = 0; - for( T it = begin; it != end; ++it ) - { - if( *it == p ) - return n; - - /* If it's not playable in this mode, don't increment. It might result in - * different output in different modes, but that's better than having holes. */ - if( !(*it)->IsPlayableIn( GAMESTATE->GetCurrentStyle()->m_StepsType ) ) - continue; - if( (*it)->GetPlayMode() != pm ) - continue; - ++n; - } - - return -1; -} - -int SongManager::GetSongRank(Song* pSong) -{ - const int index = FindIndex( m_pPopularSongs.begin(), m_pPopularSongs.end(), pSong ); - return index; // -1 means we didn't find it -} - -// lua start -#include "LuaBinding.h" - -/** @brief Allow Lua to have access to the SongManager. */ -class LunaSongManager: public Luna -{ -public: - static int SetPreferredSongs( T* p, lua_State *L ) - { - p->UpdatePreferredSort( SArg(1), "PreferredCourses.txt" ); - return 0; - } - - static int SetPreferredCourses( T* p, lua_State *L ) - { - p->UpdatePreferredSort( "PreferredSongs.txt", SArg(1) ); - return 0; - } - static int GetAllSongs( T* p, lua_State *L ) - { - const vector &v = p->GetAllSongs(); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - static int GetAllCourses( T* p, lua_State *L ) - { - vector v; - p->GetAllCourses( v, BArg(1) ); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - - static int GetPreferredSortSongs( T* p, lua_State *L ) - { - vector v; - p->GetPreferredSortSongs(v); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - static int GetPreferredSortCourses( T* p, lua_State *L ) - { - vector v; - CourseType ct = Enum::Check(L,1); - p->GetPreferredSortCourses( ct, v, BArg(2) ); - LuaHelpers::CreateTableFromArray( v, L ); - return 1; - } - - static int FindSong( T* p, lua_State *L ) { Song *pS = p->FindSong(SArg(1)); if(pS) pS->PushSelf(L); else lua_pushnil(L); return 1; } - static int FindCourse( T* p, lua_State *L ) { Course *pC = p->FindCourse(SArg(1)); if(pC) pC->PushSelf(L); else lua_pushnil(L); return 1; } - static int GetRandomSong( T* p, lua_State *L ) { Song *pS = p->GetRandomSong(); if(pS) pS->PushSelf(L); else lua_pushnil(L); return 1; } - static int GetRandomCourse( T* p, lua_State *L ) { Course *pC = p->GetRandomCourse(); if(pC) pC->PushSelf(L); else lua_pushnil(L); return 1; } - static int GetNumSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumSongs() ); return 1; } - static int GetNumLockedSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumLockedSongs() ); return 1; } - static int GetNumUnlockedSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumUnlockedSongs() ); return 1; } - static int GetNumSelectableAndUnlockedSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumSelectableAndUnlockedSongs() ); return 1; } - static int GetNumAdditionalSongs( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumAdditionalSongs() ); return 1; } - static int GetNumSongGroups( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumSongGroups() ); return 1; } - static int GetNumCourses( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumCourses() ); return 1; } - static int GetNumAdditionalCourses( T* p, lua_State *L ){ lua_pushnumber( L, p->GetNumAdditionalCourses() ); return 1; } - static int GetNumCourseGroups( T* p, lua_State *L ) { lua_pushnumber( L, p->GetNumCourseGroups() ); return 1; } - static int GetSongFromSteps( T* p, lua_State *L ) - { - Song *pSong = NULL; - if( lua_isnil(L,1) ) { pSong = p->GetSongFromSteps( NULL ); } - else { Steps *pSteps = Luna::check(L,1); pSong = p->GetSongFromSteps( pSteps ); } - if(pSong) pSong->PushSelf(L); - else lua_pushnil(L); - return 1; - } - - static int GetExtraStageInfo( T* p, lua_State *L ) - { - bool bExtra2 = BArg( 1 ); - const Style *pStyle = Luna