diff --git a/.hgignore b/.hgignore index 19d2885f36..c948c06fe5 100644 --- a/.hgignore +++ b/.hgignore @@ -64,6 +64,7 @@ lib*_link.a Data/UserPrefs Data/uPrefs Data/GamePrefs/*.cfg +Data/Static.ini PBProject/Hardware.plist Themes/pump Themes/optical-release diff --git a/Docs/Changelog_SSCformat.txt b/Docs/Changelog_SSCformat.txt index 95b48cf7d7..e5bcf25049 100644 --- a/Docs/Changelog_SSCformat.txt +++ b/Docs/Changelog_SSCformat.txt @@ -9,6 +9,12 @@ change to JSON, but it is unsure if this will be done. Implement .ssc at your own risk. ________________________________________________________________________________ +[v0.7] - theDtTvB, Wolfman2000 +* Split Timing officially implemented. +* #SPEEDS tag for modified scroll speeds. +* #FAKES tag for ignoring judgments within a range. +* #WARPS modified so that second parameter is now relative to the start. + [v0.59] - Wolfman2000 * Typo fix: #RADARVALUES needed a semicolon at the end, not a colon. diff --git a/Docs/Changelog_sm5.txt b/Docs/Changelog_sm5.txt index b5c710aa47..8a3a998b7e 100644 --- a/Docs/Changelog_sm5.txt +++ b/Docs/Changelog_sm5.txt @@ -8,16 +8,30 @@ ________________________________________________________________________________ StepMania 5.0 $NEXT | 20110xyy -------------------------------------------------------------------------------- +2011/05/17 +---------- +* [NotesWriterSM] Write out OpenITG compatible SM files from here on out. + This includes turning Warps into Negative Stops. For those that wanted + negative BPMs specifically, my apologies, but stops are easier on math. + [Wolfman2000] + 2011/05/16 ---------- * [ProfileManager] Added ProfileWasLoadedFromMemoryCard(PlayerNumber) Lua binding. [AJ] +* Added #FAKES to the SSC format. Similar to the fake arrows, these blocks + prevent all judgments within the range. [Wolfman2000] +* Changed the syntax of #WARPS slightly. Now the second value is the relative + distance that is skipped, not the absolute value when you start hitting + arrows again. [Wolfman2000] 2011/05/15 ---------- * [ScreenGameplay] Removed hardcoded commands for Debug (give up text) and replace them with DebugStartOnCommand, DebugBackOnCommand, and DebugTweenOffCommand. [AJ] +* Added #SPEEDS to the SSC format. This multiplies your scrolling speed during + gameplay, not replaces it (see mod attacks for replacing). [Wolfman2000] 2011/05/14 ---------- diff --git a/Docs/Luadoc/Lua.xml b/Docs/Luadoc/Lua.xml index f41bb848cb..6aa9eda455 100644 --- a/Docs/Luadoc/Lua.xml +++ b/Docs/Luadoc/Lua.xml @@ -1349,6 +1349,7 @@ + @@ -1405,6 +1406,8 @@ + + diff --git a/Docs/Luadoc/LuaDocumentation.xml b/Docs/Luadoc/LuaDocumentation.xml index aa4e3c09fe..bf0978bca6 100644 --- a/Docs/Luadoc/LuaDocumentation.xml +++ b/Docs/Luadoc/LuaDocumentation.xml @@ -3367,6 +3367,9 @@ Returns the numerical difficulty of the Steps. + + Returns true if the song has significant timing changes. + Returns the complete list of RadarValues for player pn. Use to grab a specific value. @@ -3515,6 +3518,12 @@ Returns true if the TimingData contains warps. + + Returns true if the TimingData contains fake segments. + + + Returns true if the TimingData contains speed scrolling changes. + diff --git a/Docs/SimfileFormats/ssc_msd5.txt b/Docs/SimfileFormats/ssc_msd5.txt index d3b7d092c1..64bad9126e 100644 --- a/Docs/SimfileFormats/ssc_msd5.txt +++ b/Docs/SimfileFormats/ssc_msd5.txt @@ -16,9 +16,10 @@ #INSTRUMENTTRACK:; #MUSICLENGTH:; #OFFSET:; +#BPMS:; #STOPS:; #DELAYS:; -#BPMS:; +#LABELS:; #TIMESIGNATURES:; #LASTBEATHINT:; #SAMPLESTART:; @@ -43,17 +44,24 @@ // information from #NOTES moved here #STEPSTYPE:; #DESCRIPTION:; +#CHARTSTYLE:; #DIFFICULTY:; #METER:; #RADARVALUES:; #CREDIT:; // steps-based timingdata +#BPMS:; #STOPS:; #DELAYS:; -#BPMS:; #TIMESIGNATURES:; -#LASTBEATHINT:; +#TICKCOUNTS:; +#COMBOS:; +#SPEEDS:; +#FAKES:; +#LABELS:; +#ATTACKS:; // Not yet in use. +#OFFSET:; // actual step data -#NOTES:; \ No newline at end of file +#NOTES:; diff --git a/NoteSkins/pump/default/UpLeft Receptor.lua b/NoteSkins/pump/default/UpLeft Receptor.lua index 3380a0b8ff..f6738e3c27 100644 --- a/NoteSkins/pump/default/UpLeft Receptor.lua +++ b/NoteSkins/pump/default/UpLeft Receptor.lua @@ -6,7 +6,7 @@ local function Beat(self) local part = beat%1 part = clamp(part,0,0.5) local eff = scale(part,0,0.5,1,0) - if (GAMESTATE:GetSongDelay() or false) and part == 0 then eff = 0 end + if (GAMESTATE:GetSongPosition:GetDelay() or false) and part == 0 then eff = 0 end if beat < 0 then eff = 0 end diff --git a/Themes/_fallback/Languages/en.ini b/Themes/_fallback/Languages/en.ini index 89502509c8..818a5bb456 100644 --- a/Themes/_fallback/Languages/en.ini +++ b/Themes/_fallback/Languages/en.ini @@ -558,6 +558,7 @@ Baked Random=Baked Random Bar=Bar Battery=Battery Beat=Beat +Beats=Beats Big=Big Blind=Blind Blink=Blink @@ -694,6 +695,7 @@ Right=Right Roll=Roll Romanization=Transliterate SHOW=Show +Seconds=Seconds Select=Select Servers=Servers Shift Left=Shift Left @@ -845,15 +847,18 @@ Exit Edit Mode=Exit Edit Mode Edit stop=Edit stop Edit delay=Edit delay Edit Timing Data=Edit Timing Data -Edit time signature=Edit time signature (not working yet) -Edit time signature (top)=Edit time signature (beats per measure) -Edit time signature (bottom)=Edit time signature (single beat note value) +Edit time signature=Edit time signature Edit tickcount=Edit tickcount Edit combo=Edit combo Edit label=Edit label Edit warp=Edit warp +Edit speed (percent)=Edit speed (percent) +Edit speed (wait)=Edit speed (length) +Edit speed (mode)=Edit speed (mode) +Edit fake=Edit fake segment Editor options=Options EditorShowBGChangesPlay=Show Backgrounds +Erase step timing=Erase step timing Effect=Effect EnableAttackSounds=Attack Sounds EnableMineHitSound=Mine Sounds @@ -1217,15 +1222,19 @@ Can't undo - no undo data.=Can't undo - no undo data. Do you want to revert from disk?=Do you want to revert from disk? Do you want to revert to your last save?=Do you want to revert to your last save? Do you want to save changes before exiting?=Do you want to save changes before exiting? -Enter a new BPM value.=Enter a new BPM value. -Enter a new Stop value.=Enter a new Stop value. -Enter a new Delay value.=Enter a new Delay value. -Enter a new Time Signature numerator value.=Enter the number of beats per measure. -Enter a new Time Signature denominator value.=Enter the note value that represents one beat. -Enter a new Tickcount value.=Enter a new Tickcount value. -Enter a new Combo value.=Enter a new Combo value. -Enter a new Label value.=Enter a name for this section of the chart. -Enter a new Warp value.=Enter the beat you will warp to when you reach this point. +Enter a new BPM value.=Enter a new BPM value.\n\nEnter the previous value to remove. +Enter a new Stop value.=Enter a new Stop value.\n\nType "0" to remove. +Enter a new Delay value.=Enter a new Delay value.\n\nType "0" to remove. +Enter a new Time Signature.=Enter a new time signature.\nUse the format "x/y".\nEnter the previous value to remove. +Enter a new Tickcount value.=Enter a new Tickcount value.\n\nEnter the previous value to remove. +Enter a new Combo value.=Enter a new Combo value.\n\nEnter the previous value to remove. +Enter a new Label value.=Enter a name for this section of the chart.\n\nEnter a previously used label to remove. +Enter a new Warp value.=Enter the number of beats you will warp over.\n\nType "0" to remove this segment. +Enter a new Speed percent value.=Enter the ratio for speed scrolling.\nEnter the previous value to remove.\n1 is the default scroll. +Enter a new Speed wait value.=Enter how long in seconds/beats it takes to change.\n\n0 is instant. +Enter a new Speed mode value.=Enter how this segment is handled.\n\nType "0" for beats, "1" for seconds. +Enter a new Fake value.=Enter the number of beats that won't be judged.\n\nType "0" to remove this. +Are you sure you want to erase this chart's timing data?=Are you sure you want to erase this chart's timing data? Enter a new artist transliteration.=Enter a new artist transliteration. Enter a new artist.=Enter a new artist. Enter a new genre.=Enter a new genre. @@ -1246,7 +1255,7 @@ Enter a new min BPM.=Enter the minimum displayed BPM. Enter a new max BPM.=Enter the maximum displayed BPM. More than %d notes per measure is not allowed. This change has been reverted.=More than %d notes per measure is not allowed. This change has been reverted. No backgrounds available=No backgrounds available -EditHelpText=Up/Down:\n change beat\nLeft/Right:\n change snap\nNumber keys:\n add/remove\n tap note\nCreate hold note:\n Hold a number\n while moving\n Up or Down\nCreate roll note:\n Hold Shift,\n then create a\n hold note.\nSpace bar:\n Set area\n marker\nEnter:\n Area Menu\nEscape:\n Main Menu\nF1:\n Show help\n +EditHelpText=Up/Down:\n change beat\nLeft/Right:\n change snap\nNumber keys:\n add/remove\n tap note\nN and M keys:\n swap tap notes\nCreate hold note:\n Hold a number\n while moving\n Up or Down\nCreate roll note:\n Hold Shift,\n then create a\n hold note.\nSpace bar:\n Set area\n marker\nEnter:\n Area Menu\nEscape:\n Main Menu\nF4:\n Timing Menu\nF1:\n Show help\n PlayRecordHelpText=Press START to end Save successful.=Save successful. Saved as SM and DWI.=Saved as SM and DWI. @@ -1257,6 +1266,7 @@ This change creates more than %d notes in a measure.=This change creates more th This change creates notes past the end of the music and is not allowed.=This change creates notes past the end of the music and is not allowed. This will destroy all unsaved changes.=This will destroy all unsaved changes. Undo=Undo +You must be in Song Timing Mode to edit BG Changes.=You must be in Song Timing Mode to edit BG Changes. Current beat=Current beat Current second=Current second @@ -1276,6 +1286,9 @@ Mines=Mines Rolls=Rolls Lifts=Lifts Fakes=Fakes +Timing Mode=Timing Mode +Step Timing=Step Timing +Song Timing=Song Timing Beat 0 offset=Beat 0 offset Preview Start=Preview Start Preview Length=Preview Length @@ -1944,7 +1957,7 @@ Decrease/increase music offset=Decrease/increase music offset Decrease/increase sample music start=Decrease/increase sample music start Decrease/increase sample music length=Decrease/increase sample music length Play sample music=Play sample music -Add/Edit Background Change=Add/Edit Background Change +Add/Edit Background Change=Add/Edit Background Change (Song Timing Only) Insert beat and shift down=Insert beat and shift down Shift BPM changes and stops down one beat=Shift BPM changes and stops down one beat Delete beat and shift up=Delete beat and shift up @@ -1953,6 +1966,7 @@ Cycle between tap notes=Cycle between tap notes Lay mine=Lay mine Lay lift=Lay lift Add to/remove from right half=Add to/remove from right half +Switch Timing=Switch Timing Switch player (Routine only)=Switch player (Routine only) [Grade] diff --git a/Themes/_fallback/Scripts/01 alias.lua b/Themes/_fallback/Scripts/01 alias.lua index 05935425bf..126d9c69ef 100644 --- a/Themes/_fallback/Scripts/01 alias.lua +++ b/Themes/_fallback/Scripts/01 alias.lua @@ -20,4 +20,4 @@ local _screen = { h = SCREEN_HEIGHT, cx = SCREEN_CENTER_X, cy = SCREEN_CENTER_Y -} \ No newline at end of file +} diff --git a/Themes/_fallback/Scripts/01 compat.lua b/Themes/_fallback/Scripts/01 compat.lua index c2f171e1f0..dbdf4a819c 100644 --- a/Themes/_fallback/Scripts/01 compat.lua +++ b/Themes/_fallback/Scripts/01 compat.lua @@ -63,4 +63,14 @@ end if ScreenMetric then ScreenMetric = Screen.Metric -end \ No newline at end of file +end + +--[[ GameState ]] +--Aliases for old GAMESTATE timing functions. +--These have been converted to SongPosition, but most themes still use these old functions. + +function GameState:GetSongBeat() return self:GetSongPosition():GetSongBeat() end +function GameState:GetSongBeatNoOffset() return self:GetSongPosition():GetSongBeatNoOffset() end +function GameState:GetSongBPS() return self:GetSongPosition():GetCurBPS() end +function GameState:GetSongDelay() return self:GetSongPosition:GetDelay() end +function GameState:GetSongFreeze() return self:GetSongPosition:GetFreeze() end diff --git a/Themes/_fallback/Sounds/ScreenEdit switch player.redir b/Themes/_fallback/Sounds/ScreenEdit switch player.redir new file mode 100644 index 0000000000..cd59d419fb --- /dev/null +++ b/Themes/_fallback/Sounds/ScreenEdit switch player.redir @@ -0,0 +1 @@ +MusicWheel sort diff --git a/Themes/_fallback/Sounds/ScreenEdit switch timing.redir b/Themes/_fallback/Sounds/ScreenEdit switch timing.redir new file mode 100644 index 0000000000..cd59d419fb --- /dev/null +++ b/Themes/_fallback/Sounds/ScreenEdit switch timing.redir @@ -0,0 +1 @@ +MusicWheel sort diff --git a/Themes/_fallback/metrics.ini b/Themes/_fallback/metrics.ini index b80fa71588..c4dd4b0bd6 100644 --- a/Themes/_fallback/metrics.ini +++ b/Themes/_fallback/metrics.ini @@ -989,6 +989,8 @@ TimeSignatureColor=color("1,0.55,0,1") TickcountColor=color("0,1,0,1") ComboColor=color("0.55,1,0,1") LabelColor=color("1,0,0,1") +SpeedColor=color("0.5,1,1,1") +FakeColor=color("1,1,0.5,1") # BPMIsLeftSide=true StopIsLeftSide=true @@ -998,15 +1000,19 @@ TimeSignatureIsLeftSide=true TickcountIsLeftSide=false ComboIsLeftSide=false LabelIsLeftSide=false +SpeedIsLeftSide=false +FakeIsLeftSide=true # BPMOffsetX=60 StopOffsetX=50 DelayOffsetX=120 -WarpOffsetX=60 +WarpOffsetX=90 TimeSignatureOffsetX=30 -TickcountOffsetX=10 -ComboOffsetX=30 -LabelOffsetX=80 +TickcountOffsetX=70 +ComboOffsetX=50 +LabelOffsetX=130 +SpeedOffsetX=30 +FakeOffsetX=90 [PlayerStageStats] # Original CVS Grading @@ -3607,7 +3613,7 @@ TimerSeconds=-1 EditModifiers="no reverse" EditHelpX=SCREEN_LEFT+4 EditHelpY=SCREEN_TOP+16 -EditHelpOnCommand=halign,0;valign,0;zoom,0.675;shadowlength,1 +EditHelpOnCommand=halign,0;valign,0;zoom,0.5;shadowlength,1 InfoX=SCREEN_RIGHT-128 InfoY=SCREEN_TOP+16 InfoOnCommand=halign,0;valign,0;zoom,0.5;shadowlength,1 @@ -3636,6 +3642,7 @@ NumHandsFormat="%s: %d\n" NumRollsFormat="%s: %d\n" NumLiftsFormat="%s: %d\n" NumFakesFormat="%s: %d\n" +TimingModeFormat="%s:\n %s\n" Beat0OffsetFormat="%s:\n %.5f secs\n" PreviewStartFormat="%s:\n %.5f secs\n" PreviewLengthFormat="%s:\n %.5f secs\n" diff --git a/Xcode/stepmania.xcodeproj/project.pbxproj b/Xcode/stepmania.xcodeproj/project.pbxproj index b90859f4e1..f8a7209385 100644 --- a/Xcode/stepmania.xcodeproj/project.pbxproj +++ b/Xcode/stepmania.xcodeproj/project.pbxproj @@ -1657,6 +1657,8 @@ AAFF41A40F5BD4AD00FFDB67 /* ScreenOptionsExportPackage.h in Headers */ = {isa = PBXBuildFile; fileRef = AAFF41A20F5BD4AD00FFDB67 /* ScreenOptionsExportPackage.h */; }; AAFF41A50F5BD4AD00FFDB67 /* ScreenOptionsExportPackage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = AAFF41A10F5BD4AD00FFDB67 /* ScreenOptionsExportPackage.cpp */; }; AAFF41A60F5BD4AD00FFDB67 /* ScreenOptionsExportPackage.h in Headers */ = {isa = PBXBuildFile; fileRef = AAFF41A20F5BD4AD00FFDB67 /* ScreenOptionsExportPackage.h */; }; + B75CE3B5137963350076A82D /* SongPosition.h in Headers */ = {isa = PBXBuildFile; fileRef = B75CE3B3137963350076A82D /* SongPosition.h */; }; + B75CE3B6137963350076A82D /* SongPosition.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B75CE3B4137963350076A82D /* SongPosition.cpp */; }; EE2153A910A75BEE0074B01B /* NotesLoaderPMS.cpp in Sources */ = {isa = PBXBuildFile; fileRef = EE2153A710A75BEE0074B01B /* NotesLoaderPMS.cpp */; }; EE2153AA10A75BEE0074B01B /* NotesLoaderPMS.h in Headers */ = {isa = PBXBuildFile; fileRef = EE2153A810A75BEE0074B01B /* NotesLoaderPMS.h */; }; EE879B60132DEE720078FDDC /* RageDisplay_Legacy_Helpers.cpp in Sources */ = {isa = PBXBuildFile; fileRef = EE879B5C132DEE720078FDDC /* RageDisplay_Legacy_Helpers.cpp */; }; @@ -3025,6 +3027,8 @@ AAFF55220549F43A00D02EE9 /* ComboGraph.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; name = ComboGraph.h; path = ../src/ComboGraph.h; sourceTree = ""; }; AAFF5541054E736D00D02EE9 /* Attack.cpp */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.cpp; name = Attack.cpp; path = ../src/Attack.cpp; sourceTree = ""; }; AAFF5542054E736D00D02EE9 /* Attack.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.cpp.h; name = Attack.h; path = ../src/Attack.h; sourceTree = ""; }; + B75CE3B3137963350076A82D /* SongPosition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SongPosition.h; path = ../src/SongPosition.h; sourceTree = SOURCE_ROOT; }; + B75CE3B4137963350076A82D /* SongPosition.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = SongPosition.cpp; path = ../src/SongPosition.cpp; sourceTree = SOURCE_ROOT; }; EE2153A710A75BEE0074B01B /* NotesLoaderPMS.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = NotesLoaderPMS.cpp; path = ../src/NotesLoaderPMS.cpp; sourceTree = SOURCE_ROOT; }; EE2153A810A75BEE0074B01B /* NotesLoaderPMS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NotesLoaderPMS.h; path = ../src/NotesLoaderPMS.h; sourceTree = SOURCE_ROOT; }; EE879B5C132DEE720078FDDC /* RageDisplay_Legacy_Helpers.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = RageDisplay_Legacy_Helpers.cpp; path = ../src/RageDisplay_Legacy_Helpers.cpp; sourceTree = SOURCE_ROOT; }; @@ -3938,6 +3942,8 @@ AA7CDD930D8395F90001DAFC /* Song.h */, AA6EFE0704770C79005F316C /* SongOptions.cpp */, AA6EFE0804770C79005F316C /* SongOptions.h */, + B75CE3B4137963350076A82D /* SongPosition.cpp */, + B75CE3B3137963350076A82D /* SongPosition.h */, AA5C73F90637A5F000AFB891 /* SongUtil.cpp */, AA5C73FA0637A5F000AFB891 /* SongUtil.h */, AAEBF57C0B5C83F4005F3C7C /* SoundEffectControl.cpp */, @@ -5056,6 +5062,7 @@ 2BC6132D13773C8E00CC8BBD /* pngpriv.h in Headers */, 2BC6133313773C8E00CC8BBD /* pngstruct.h in Headers */, 2BC6133E13773D2C00CC8BBD /* pnglibconf.h in Headers */, + B75CE3B5137963350076A82D /* SongPosition.h in Headers */, 2B9C2E8E137A138B004C92F3 /* NotesLoaderJson.h in Headers */, 2B9C2E90137A138B004C92F3 /* NotesWriterJson.h in Headers */, ); @@ -6317,6 +6324,7 @@ 2BC6133813773C8E00CC8BBD /* pngwrite.c in Sources */, 2BC6133913773C8E00CC8BBD /* pngwtran.c in Sources */, 2BC6133A13773C8E00CC8BBD /* pngwutil.c in Sources */, + B75CE3B6137963350076A82D /* SongPosition.cpp in Sources */, 2B9C2E8D137A138B004C92F3 /* NotesLoaderJson.cpp in Sources */, 2B9C2E8F137A138B004C92F3 /* NotesWriterJson.cpp in Sources */, ); diff --git a/src/Actor.cpp b/src/Actor.cpp index 72a3ee3856..3191d98d56 100644 --- a/src/Actor.cpp +++ b/src/Actor.cpp @@ -34,6 +34,8 @@ REGISTER_ACTOR_CLASS_WITH_NAME( HiddenActor, Actor ); float Actor::g_fCurrentBGMTime = 0, Actor::g_fCurrentBGMBeat; float Actor::g_fCurrentBGMTimeNoOffset = 0, Actor::g_fCurrentBGMBeatNoOffset = 0; +vector Actor::g_vfCurrentBGMBeatPlayer(NUM_PlayerNumber, 0); +vector Actor::g_vfCurrentBGMBeatPlayerNoOffset(NUM_PlayerNumber, 0); Actor *Actor::Copy() const { return new Actor(*this); } @@ -68,6 +70,12 @@ void Actor::SetBGMTime( float fTime, float fBeat, float fTimeNoOffset, float fBe g_fCurrentBGMBeatNoOffset = fBeatNoOffset; } +void Actor::SetPlayerBGMBeat( PlayerNumber pn, float fBeat, float fBeatNoOffset ) +{ + g_vfCurrentBGMBeatPlayer[pn] = fBeat; + g_vfCurrentBGMBeatPlayerNoOffset[pn] = fBeatNoOffset; +} + void Actor::SetBGMLight( int iLightNumber, float fCabinetLights ) { ASSERT( iLightNumber < NUM_CabinetLight ); @@ -150,8 +158,7 @@ Actor::Actor() lua_setfield( L, -2, "ctx" ); lua_pop( L, 1 ); LUA->Release( L ); - - + m_size = RageVector2( 1, 1 ); InitState(); m_pParent = NULL; @@ -676,6 +683,16 @@ void Actor::UpdateInternal( float fDeltaTime ) m_fSecsIntoEffect = g_fCurrentBGMBeat; break; + case CLOCK_BGM_BEAT_PLAYER1: + m_fEffectDelta = g_vfCurrentBGMBeatPlayer[PLAYER_1] - m_fSecsIntoEffect; + m_fSecsIntoEffect = g_vfCurrentBGMBeatPlayerNoOffset[PLAYER_1]; + break; + + case CLOCK_BGM_BEAT_PLAYER2: + m_fEffectDelta = g_vfCurrentBGMBeatPlayer[PLAYER_2] - m_fSecsIntoEffect; + m_fSecsIntoEffect = g_vfCurrentBGMBeatPlayerNoOffset[PLAYER_2]; + break; + case CLOCK_BGM_TIME: m_fEffectDelta = g_fCurrentBGMTime - m_fSecsIntoEffect; m_fSecsIntoEffect = g_fCurrentBGMTime; diff --git a/src/Actor.h b/src/Actor.h index 45d080dabe..919d4994f7 100644 --- a/src/Actor.h +++ b/src/Actor.h @@ -1,6 +1,7 @@ #ifndef ACTOR_H #define ACTOR_H +#include "PlayerNumber.h" #include "RageTypes.h" #include "RageUtil_AutoPtr.h" #include "LuaReference.h" @@ -109,6 +110,7 @@ public: virtual void LoadFromNode( const XNode* pNode ); static void SetBGMTime( float fTime, float fBeat, float fTimeNoOffset, float fBeatNoOffset ); + static void SetPlayerBGMBeat( PlayerNumber pn, float fBeat, float fBeatNoOffset ); static void SetBGMLight( int iLightNumber, float fCabinetLights ); /** @@ -131,6 +133,8 @@ public: CLOCK_BGM_BEAT, CLOCK_BGM_TIME_NO_OFFSET, CLOCK_BGM_BEAT_NO_OFFSET, + CLOCK_BGM_BEAT_PLAYER1, + CLOCK_BGM_BEAT_PLAYER2, CLOCK_LIGHT_1 = 1000, CLOCK_LIGHT_LAST = 1100, NUM_CLOCKS @@ -713,6 +717,8 @@ protected: // global state static float g_fCurrentBGMTime, g_fCurrentBGMBeat; static float g_fCurrentBGMTimeNoOffset, g_fCurrentBGMBeatNoOffset; + static vector g_vfCurrentBGMBeatPlayer; + static vector g_vfCurrentBGMBeatPlayerNoOffset; private: // commands diff --git a/src/AdjustSync.cpp b/src/AdjustSync.cpp index e669f9cb74..b375928a25 100644 --- a/src/AdjustSync.cpp +++ b/src/AdjustSync.cpp @@ -58,7 +58,7 @@ void AdjustSync::ResetOriginalSyncData() s_pTimingDataOriginal = new TimingData; if( GAMESTATE->m_pCurSong ) - *s_pTimingDataOriginal = GAMESTATE->m_pCurSong->m_Timing; + *s_pTimingDataOriginal = GAMESTATE->m_pCurSong->m_SongTiming; else *s_pTimingDataOriginal = TimingData(); s_fGlobalOffsetSecondsOriginal = PREFSMAN->m_fGlobalOffsetSeconds; @@ -87,7 +87,7 @@ void AdjustSync::SaveSyncChanges() { if( GAMESTATE->IsCourseMode() ) return; - if( GAMESTATE->m_pCurSong && *s_pTimingDataOriginal != GAMESTATE->m_pCurSong->m_Timing ) + if( GAMESTATE->m_pCurSong && *s_pTimingDataOriginal != GAMESTATE->m_pCurSong->m_SongTiming ) { if( GAMESTATE->IsEditing() ) { @@ -110,7 +110,7 @@ void AdjustSync::RevertSyncChanges() if( GAMESTATE->IsCourseMode() ) return; PREFSMAN->m_fGlobalOffsetSeconds.Set( s_fGlobalOffsetSecondsOriginal ); - GAMESTATE->m_pCurSong->m_Timing = *s_pTimingDataOriginal; + GAMESTATE->m_pCurSong->m_SongTiming = *s_pTimingDataOriginal; ResetOriginalSyncData(); s_fStandardDeviation = 0.0f; s_fAverageError = 0.0f; @@ -186,7 +186,7 @@ void AdjustSync::AutosyncOffset() switch( GAMESTATE->m_SongOptions.GetCurrent().m_AutosyncType ) { case SongOptions::AUTOSYNC_SONG: - GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds += mean; + GAMESTATE->m_pCurSong->m_SongTiming.m_fBeat0OffsetInSeconds += mean; break; case SongOptions::AUTOSYNC_MACHINE: PREFSMAN->m_fGlobalOffsetSeconds.Set( PREFSMAN->m_fGlobalOffsetSeconds + mean ); @@ -232,15 +232,15 @@ void AdjustSync::AutosyncTempo() if( !CalcLeastSquares( s_vAutosyncTempoData, fSlope, fIntercept, fFilteredError ) ) return; - GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds += fIntercept; + GAMESTATE->m_pCurSong->m_SongTiming.m_fBeat0OffsetInSeconds += fIntercept; const float fScaleBPM = 1.0f/(1.0f - fSlope); - FOREACH( BPMSegment, GAMESTATE->m_pCurSong->m_Timing.m_BPMSegments, i ) + FOREACH( BPMSegment, GAMESTATE->m_pCurSong->m_SongTiming.m_BPMSegments, i ) i->SetBPM( i->GetBPM() * fScaleBPM ); // We assume that the stops were measured as a number of beats. // Therefore, if we change the bpms, we need to make a similar // change to the stops. - FOREACH( StopSegment, GAMESTATE->m_pCurSong->m_Timing.m_StopSegments, i ) + FOREACH( StopSegment, GAMESTATE->m_pCurSong->m_SongTiming.m_StopSegments, i ) i->m_fStopSeconds *= 1.0f - fSlope; SCREENMAN->SystemMessage( AUTOSYNC_CORRECTION_APPLIED.GetValue() ); @@ -296,7 +296,7 @@ void AdjustSync::GetSyncChangeTextSong( vector &vsAddTo ) { float fOld = Quantize( AdjustSync::s_pTimingDataOriginal->m_fBeat0OffsetInSeconds, 0.001f ); - float fNew = Quantize( GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds, 0.001f ); + float fNew = Quantize( GAMESTATE->m_pCurSong->m_SongTiming.m_fBeat0OffsetInSeconds, 0.001f ); float fDelta = fNew - fOld; if( fabsf(fDelta) > 0.0001f ) @@ -309,10 +309,10 @@ void AdjustSync::GetSyncChangeTextSong( vector &vsAddTo ) } } - for( unsigned i=0; im_pCurSong->m_Timing.m_BPMSegments.size(); i++ ) + for( unsigned i=0; im_pCurSong->m_SongTiming.m_BPMSegments.size(); i++ ) { float fOld = Quantize( AdjustSync::s_pTimingDataOriginal->m_BPMSegments[i].GetBPM(), 0.001f ); - float fNew = Quantize( GAMESTATE->m_pCurSong->m_Timing.m_BPMSegments[i].GetBPM(), 0.001f ); + float fNew = Quantize( GAMESTATE->m_pCurSong->m_SongTiming.m_BPMSegments[i].GetBPM(), 0.001f ); float fDelta = fNew - fOld; if( fabsf(fDelta) > 0.0001f ) @@ -330,10 +330,10 @@ void AdjustSync::GetSyncChangeTextSong( vector &vsAddTo ) } } - for( unsigned i=0; im_pCurSong->m_Timing.m_StopSegments.size(); i++ ) + for( unsigned i=0; im_pCurSong->m_SongTiming.m_StopSegments.size(); i++ ) { float fOld = Quantize( AdjustSync::s_pTimingDataOriginal->m_StopSegments[i].m_fStopSeconds, 0.001f ); - float fNew = Quantize( GAMESTATE->m_pCurSong->m_Timing.m_StopSegments[i].m_fStopSeconds, 0.001f ); + float fNew = Quantize( GAMESTATE->m_pCurSong->m_SongTiming.m_StopSegments[i].m_fStopSeconds, 0.001f ); float fDelta = fNew - fOld; if( fabsf(fDelta) > 0.0001f ) diff --git a/src/ArrowEffects.cpp b/src/ArrowEffects.cpp index 7df7a37015..e408967789 100644 --- a/src/ArrowEffects.cpp +++ b/src/ArrowEffects.cpp @@ -67,13 +67,13 @@ static float GetNoteFieldHeight( const PlayerState* pPlayerState ) namespace { - float g_fExpandSeconds = 0; struct PerPlayerData { float m_fMinTornadoX[MAX_COLS_PER_PLAYER]; float m_fMaxTornadoX[MAX_COLS_PER_PLAYER]; float m_fInvertDistance[MAX_COLS_PER_PLAYER]; float m_fBeatFactor; + float m_fExpandSeconds; }; PerPlayerData g_EffectData[NUM_PLAYERS]; }; @@ -82,22 +82,25 @@ void ArrowEffects::Update() { const Style* pStyle = GAMESTATE->GetCurrentStyle(); - { - static float fLastTime = 0; - float fTime = RageTimer::GetTimeSinceStartFast(); - if( !GAMESTATE->m_bFreeze || !GAMESTATE->m_bDelay ) - { - g_fExpandSeconds += fTime - fLastTime; - g_fExpandSeconds = fmodf( g_fExpandSeconds, PI*2 ); - } - fLastTime = fTime; - } - FOREACH_PlayerNumber( pn ) { const Style::ColumnInfo* pCols = pStyle->m_ColumnInfo[pn]; + const SongPosition &position = GAMESTATE->m_bIsUsingStepTiming + ? GAMESTATE->m_pPlayerState[pn]->m_Position : GAMESTATE->m_Position; PerPlayerData &data = g_EffectData[pn]; + + { + static float fLastTime = 0; + float fTime = RageTimer::GetTimeSinceStartFast(); + if( !position.m_bFreeze || !position.m_bDelay ) + { + data.m_fExpandSeconds += fTime - fLastTime; + data.m_fExpandSeconds = fmodf( data.m_fExpandSeconds, PI*2 ); + } + fLastTime = fTime; + } + // Update Tornado for( int iColNum = 0; iColNum < MAX_COLS_PER_PLAYER; ++iColNum ) { @@ -176,7 +179,7 @@ void ArrowEffects::Update() // Update Beat do { float fAccelTime = 0.2f, fTotalTime = 0.5f; - float fBeat = GAMESTATE->m_fSongBeatVisible + fAccelTime; + float fBeat = position.m_fSongBeatVisible + fAccelTime; const bool bEvenBeat = ( int(fBeat) % 2 ) != 0; @@ -208,6 +211,50 @@ void ArrowEffects::Update() } } +float GetSpeedMultiplier( float fSongBeat, float fMusicSeconds, const TimingData &tim ) +{ + if( tim.m_SpeedSegments.size() == 0 ) + return 1.0; + + const int index = tim.GetSpeedSegmentIndexAtBeat( fSongBeat ); + + const SpeedSegment &seg = tim.m_SpeedSegments[index]; + float fStartBeat = NoteRowToBeat(seg.m_iStartRow); + float fStartTime = tim.GetElapsedTimeFromBeat( fStartBeat ) - tim.GetDelayAtBeat( fStartBeat ); + float fEndTime; + float fCurTime = fMusicSeconds; + + if( seg.m_usMode == 1 ) // seconds + { + fEndTime = fStartTime + seg.m_fWait; + } + else + { + fEndTime = tim.GetElapsedTimeFromBeat( fStartBeat + seg.m_fWait ) - tim.GetDelayAtBeat( fStartBeat + seg.m_fWait ); + } + + if( ( index == 0 && tim.m_SpeedSegments[0].m_fWait > 0.0 ) && fCurTime < fStartTime ) + { + return 1.0; + } + else if( fEndTime >= fCurTime && ( index > 0 || tim.m_SpeedSegments[0].m_fWait > 0.0 ) ) + { + const float fPriorSpeed = ( index == 0 ? 1 : tim.m_SpeedSegments[index - 1].m_fPercent ); + float fTimeUsed = fCurTime - fStartTime; + float fDuration = fEndTime - fStartTime; + float fRatioUsed = fDuration == 0.0 ? 1 : fTimeUsed / fDuration; + + float fDistance = fPriorSpeed - seg.m_fPercent; + float fRatioNeed = fRatioUsed * -fDistance; + return (fPriorSpeed + fRatioNeed); + } + else + { + return seg.m_fPercent; + } + +} + /* For visibility testing: if bAbsolute is false, random modifiers must return * the minimum possible scroll speed. */ float ArrowEffects::GetYOffset( const PlayerState* pPlayerState, int iCol, float fNoteBeat, float &fPeakYOffsetOut, bool &bIsPastPeakOut, bool bAbsolute ) @@ -217,21 +264,27 @@ float ArrowEffects::GetYOffset( const PlayerState* pPlayerState, int iCol, float bIsPastPeakOut = true; float fYOffset = 0; + const SongPosition &position = GAMESTATE->m_bIsUsingStepTiming + ? pPlayerState->m_Position : GAMESTATE->m_Position; + + float fSongBeat = position.m_fSongBeatVisible; + + Steps *pCurSteps = GAMESTATE->m_pCurSteps[pPlayerState->m_PlayerNumber]; /* Usually, fTimeSpacing is 0 or 1, in which case we use entirely beat spacing or * entirely time spacing (respectively). Occasionally, we tween between them. */ if( pPlayerState->m_PlayerOptions.GetCurrent().m_fTimeSpacing != 1.0f ) { - float fSongBeat = GAMESTATE->m_fSongBeatVisible; float fBeatsUntilStep = fNoteBeat - fSongBeat; float fYOffsetBeatSpacing = fBeatsUntilStep; - fYOffset += fYOffsetBeatSpacing * (1-pPlayerState->m_PlayerOptions.GetCurrent().m_fTimeSpacing); + float fSpeedMultiplier = ( GAMESTATE->m_bInStepEditor || !GAMESTATE->m_bIsUsingStepTiming ) ? 1.0 : GetSpeedMultiplier( position.m_fSongBeatVisible, position.m_fMusicSecondsVisible, pCurSteps->m_Timing ); + fYOffset += fSpeedMultiplier * fYOffsetBeatSpacing * (1-pPlayerState->m_PlayerOptions.GetCurrent().m_fTimeSpacing); } if( pPlayerState->m_PlayerOptions.GetCurrent().m_fTimeSpacing != 0.0f ) { - float fSongSeconds = GAMESTATE->m_fMusicSecondsVisible; - float fNoteSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fNoteBeat); + float fSongSeconds = GAMESTATE->m_Position.m_fMusicSecondsVisible; + float fNoteSeconds = pCurSteps->m_Timing.GetElapsedTimeFromBeat(fNoteBeat); float fSecondsUntilStep = fNoteSeconds - fSongSeconds; float fBPM = pPlayerState->m_PlayerOptions.GetCurrent().m_fScrollBPM; float fBPS = fBPM/60.f; @@ -242,7 +295,7 @@ float ArrowEffects::GetYOffset( const PlayerState* pPlayerState, int iCol, float // TODO: If we allow noteskins to have metricable row spacing // (per issue 24), edit this to reflect that. -aj fYOffset *= ARROW_SPACING; - + // don't mess with the arrows after they've crossed 0 if( fYOffset < 0 ) return fYOffset * pPlayerState->m_PlayerOptions.GetCurrent().m_fScrollSpeed; @@ -288,6 +341,7 @@ float ArrowEffects::GetYOffset( const PlayerState* pPlayerState, int iCol, float // Factor in scroll speed float fScrollSpeed = pPlayerState->m_PlayerOptions.GetCurrent().m_fScrollSpeed; + if( pPlayerState->m_PlayerOptions.GetCurrent().m_fRandomSpeed > 0 && !bAbsolute ) { // Generate a deterministically "random" speed for each arrow. @@ -307,7 +361,10 @@ float ArrowEffects::GetYOffset( const PlayerState* pPlayerState, int iCol, float if( fAccels[PlayerOptions::ACCEL_EXPAND] != 0 ) { - float fExpandMultiplier = SCALE( RageFastCos(g_fExpandSeconds*EXPAND_MULTIPLIER_FREQUENCY), + // TODO: Don't index by PlayerNumber. + PerPlayerData &data = g_EffectData[pPlayerState->m_PlayerNumber]; + + float fExpandMultiplier = SCALE( RageFastCos(data.m_fExpandSeconds*EXPAND_MULTIPLIER_FREQUENCY), EXPAND_MULTIPLIER_SCALE_FROM_LOW, EXPAND_MULTIPLIER_SCALE_FROM_HIGH, EXPAND_MULTIPLIER_SCALE_TO_LOW, EXPAND_MULTIPLIER_SCALE_TO_HIGH ); fScrollSpeed *= SCALE( fAccels[PlayerOptions::ACCEL_EXPAND], @@ -511,7 +568,7 @@ float ArrowEffects::GetRotationZ( const PlayerState* pPlayerState, float fNoteBe // As usual, enable dizzy hold heads at your own risk. -Wolfman2000 if( fEffects[PlayerOptions::EFFECT_DIZZY] != 0 && ( DIZZY_HOLD_HEADS || !bIsHoldHead ) ) { - const float fSongBeat = GAMESTATE->m_fSongBeatVisible; + const float fSongBeat = pPlayerState->m_Position.m_fSongBeatVisible; float fDizzyRotation = fNoteBeat - fSongBeat; fDizzyRotation *= fEffects[PlayerOptions::EFFECT_DIZZY]; fDizzyRotation = fmodf( fDizzyRotation, 2*PI ); @@ -528,7 +585,7 @@ float ArrowEffects::ReceptorGetRotationZ( const PlayerState* pPlayerState ) if( fEffects[PlayerOptions::EFFECT_CONFUSION] != 0 ) { - float fConfRotation = GAMESTATE->m_fSongBeatVisible; + float fConfRotation = pPlayerState->m_Position.m_fSongBeatVisible; fConfRotation *= fEffects[PlayerOptions::EFFECT_CONFUSION]; fConfRotation = fmodf( fConfRotation, 2*PI ); fConfRotation *= -180/PI; @@ -680,7 +737,7 @@ float ArrowEffects::GetBrightness( const PlayerState* pPlayerState, float fNoteB if( GAMESTATE->IsEditing() ) return 1; - float fSongBeat = GAMESTATE->m_fSongBeatVisible; + float fSongBeat = pPlayerState->m_Position.m_fSongBeatVisible; float fBeatsUntilStep = fNoteBeat - fSongBeat; float fBrightness = SCALE( fBeatsUntilStep, 0, -1, 1.f, 0.f ); diff --git a/src/Attack.cpp b/src/Attack.cpp index a896944158..1bf2c8623b 100644 --- a/src/Attack.cpp +++ b/src/Attack.cpp @@ -11,9 +11,10 @@ void Attack::GetAttackBeats( const Song *pSong, float &fStartBeat, float &fEndBe { ASSERT( pSong ); ASSERT_M( fStartSecond >= 0, ssprintf("StartSecond: %f",fStartSecond) ); - - fStartBeat = pSong->GetBeatFromElapsedTime( fStartSecond ); - fEndBeat = pSong->GetBeatFromElapsedTime( fStartSecond+fSecsRemaining ); + + const TimingData &timing = pSong->m_SongTiming; + fStartBeat = timing.GetBeatFromElapsedTime( fStartSecond ); + fEndBeat = timing.GetBeatFromElapsedTime( fStartSecond+fSecsRemaining ); } /* Get the range for an attack that's being applied in realtime, eg. during battle @@ -31,12 +32,13 @@ void Attack::GetRealtimeAttackBeats( const Song *pSong, const PlayerState* pPlay ASSERT( pSong ); /* If reasonable, push the attack forward 8 beats so that notes on screen don't change suddenly. */ - fStartBeat = min( GAMESTATE->m_fSongBeat+8, pPlayerState->m_fLastDrawnBeat ); + fStartBeat = min( GAMESTATE->m_Position.m_fSongBeat+8, pPlayerState->m_fLastDrawnBeat ); fStartBeat = truncf(fStartBeat)+1; - const float lStartSecond = pSong->GetElapsedTimeFromBeat( fStartBeat ); + const TimingData &timing = pSong->m_SongTiming; + const float lStartSecond = timing.GetElapsedTimeFromBeat( fStartBeat ); const float fEndSecond = lStartSecond + fSecsRemaining; - fEndBeat = pSong->GetBeatFromElapsedTime( fEndSecond ); + fEndBeat = timing.GetBeatFromElapsedTime( fEndSecond ); fEndBeat = truncf(fEndBeat)+1; // loading the course should have caught this. diff --git a/src/AutoKeysounds.cpp b/src/AutoKeysounds.cpp index 5aac67976b..79ec773973 100644 --- a/src/AutoKeysounds.cpp +++ b/src/AutoKeysounds.cpp @@ -99,7 +99,7 @@ void AutoKeysounds::LoadAutoplaySoundsInto( RageSoundReader_Chain *pChain ) if( tn[pn].iKeysoundIndex >= 0 ) { RString sKeysoundFilePath = sSongDir + pSong->m_vsKeysoundFile[tn[pn].iKeysoundIndex]; - float fSeconds = pSong->m_Timing.GetElapsedTimeFromBeatNoOffset( NoteRowToBeat(iRow) ) + SOUNDMAN->GetPlayLatency(); + float fSeconds = GAMESTATE->m_pCurSteps[pn]->m_Timing.GetElapsedTimeFromBeatNoOffset( NoteRowToBeat(iRow) ) + SOUNDMAN->GetPlayLatency(); float fPan = 0; if( !bSoundIsGlobal ) diff --git a/src/BPMDisplay.cpp b/src/BPMDisplay.cpp index f4b27d51b2..49cb155978 100644 --- a/src/BPMDisplay.cpp +++ b/src/BPMDisplay.cpp @@ -277,7 +277,7 @@ SongBPMDisplay::SongBPMDisplay() void SongBPMDisplay::Update( float fDeltaTime ) { - float fGameStateBPM = GAMESTATE->m_fCurBPS * 60.0f; + float fGameStateBPM = GAMESTATE->m_Position.m_fCurBPS * 60.0f; if( m_fLastGameStateBPM != fGameStateBPM ) { m_fLastGameStateBPM = fGameStateBPM; diff --git a/src/Background.cpp b/src/Background.cpp index 34050aedcc..8c5a64c5fa 100644 --- a/src/Background.cpp +++ b/src/Background.cpp @@ -419,7 +419,7 @@ void BackgroundImpl::LoadFromRandom( float fFirstBeat, float fEndBeat, const Bac int iStartRow = BeatToNoteRow(fFirstBeat); int iEndRow = BeatToNoteRow(fEndBeat); - const TimingData &timing = m_pSong->m_Timing; + const TimingData &timing = m_pSong->m_SongTiming; // change BG every time signature change or 4 measures FOREACH_CONST( TimeSignatureSegment, timing.m_vTimeSignatureSegments, iter ) @@ -697,7 +697,7 @@ void BackgroundImpl::Layer::UpdateCurBGChange( const Song *pSong, float fLastMus float fBeat, fBPS, fThrowAway; bool bFreeze; int iThrowAway; - pSong->m_Timing.GetBeatAndBPSFromElapsedTime( fCurrentTime, fBeat, fBPS, bFreeze, bFreeze, iThrowAway, fThrowAway ); + pSong->m_SongTiming.GetBeatAndBPSFromElapsedTime( fCurrentTime, fBeat, fBPS, bFreeze, bFreeze, iThrowAway, fThrowAway ); // Calls to Update() should *not* be scaled by music rate; fCurrentTime is. Undo it. const float fRate = GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; @@ -762,7 +762,7 @@ void BackgroundImpl::Layer::UpdateCurBGChange( const Song *pSong, float fLastMus m_pCurrentBGA->PlayCommand( "GainFocus" ); /* How much time of this BGA have we skipped? (This happens with SetSeconds.) */ - const float fStartSecond = pSong->m_Timing.GetElapsedTimeFromBeat( change.m_fStartBeat ); + const float fStartSecond = pSong->m_SongTiming.GetElapsedTimeFromBeat( change.m_fStartBeat ); /* This is affected by the music rate. */ fDeltaTime = fCurrentTime - fStartSecond; @@ -800,9 +800,9 @@ void BackgroundImpl::Update( float fDeltaTime ) FOREACH_BackgroundLayer( i ) { Layer &layer = m_Layer[i]; - layer.UpdateCurBGChange( m_pSong, m_fLastMusicSeconds, GAMESTATE->m_fMusicSeconds, m_mapNameToTransition ); + layer.UpdateCurBGChange( m_pSong, m_fLastMusicSeconds, GAMESTATE->m_Position.m_fMusicSeconds, m_mapNameToTransition ); } - m_fLastMusicSeconds = GAMESTATE->m_fMusicSeconds; + m_fLastMusicSeconds = GAMESTATE->m_Position.m_fMusicSeconds; } void BackgroundImpl::DrawPrimitives() diff --git a/src/BeginnerHelper.cpp b/src/BeginnerHelper.cpp index 3bf23107df..9bc73247ba 100644 --- a/src/BeginnerHelper.cpp +++ b/src/BeginnerHelper.cpp @@ -191,7 +191,7 @@ void BeginnerHelper::ShowStepCircle( PlayerNumber pn, int CSTEP ) m_sStepCircle[pn][isc].StopEffect(); m_sStepCircle[pn][isc].SetZoom( 2 ); m_sStepCircle[pn][isc].StopTweening(); - m_sStepCircle[pn][isc].BeginTweening( GAMESTATE->m_fCurBPS/3, TWEEN_LINEAR ); + m_sStepCircle[pn][isc].BeginTweening( GAMESTATE->m_Position.m_fCurBPS/3, TWEEN_LINEAR ); m_sStepCircle[pn][isc].SetZoom( 0 ); } @@ -318,19 +318,19 @@ void BeginnerHelper::Step( PlayerNumber pn, int CSTEP ) ShowStepCircle( pn, ST_DOWN ); m_pDancer[pn]->StopTweening(); m_pDancer[pn]->PlayAnimation( "Step-JUMPLR", 1.5f ); - m_pDancer[pn]->BeginTweening( GAMESTATE->m_fCurBPS/8, TWEEN_LINEAR ); + m_pDancer[pn]->BeginTweening( GAMESTATE->m_Position.m_fCurBPS/8, TWEEN_LINEAR ); m_pDancer[pn]->SetRotationY( 90 ); - m_pDancer[pn]->BeginTweening( 1/(GAMESTATE->m_fCurBPS * 2) ); //sleep between jump-frames - m_pDancer[pn]->BeginTweening( GAMESTATE->m_fCurBPS /6, TWEEN_LINEAR ); + m_pDancer[pn]->BeginTweening( 1/(GAMESTATE->m_Position.m_fCurBPS * 2) ); //sleep between jump-frames + m_pDancer[pn]->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /6, TWEEN_LINEAR ); m_pDancer[pn]->SetRotationY( 0 ); break; } m_sFlash.StopEffect(); m_sFlash.StopTweening(); - m_sFlash.Sleep( GAMESTATE->m_fCurBPS/16 ); + m_sFlash.Sleep( GAMESTATE->m_Position.m_fCurBPS/16 ); m_sFlash.SetDiffuseAlpha( 1 ); - m_sFlash.BeginTweening( 1/GAMESTATE->m_fCurBPS * 0.5f ); + m_sFlash.BeginTweening( 1/GAMESTATE->m_Position.m_fCurBPS * 0.5f ); m_sFlash.SetDiffuseAlpha( 0 ); } @@ -340,7 +340,7 @@ void BeginnerHelper::Update( float fDeltaTime ) return; // the row we want to check on this update - int iCurRow = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat + 0.4f ); + int iCurRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat + 0.4f ); FOREACH_EnabledPlayer( pn ) { for( int iRow=m_iLastRowChecked; iRowUpdate( fDeltaTime ); m_sFlash.Update( fDeltaTime ); - float beat = fDeltaTime*GAMESTATE->m_fCurBPS; + float beat = fDeltaTime*GAMESTATE->m_Position.m_fCurBPS; // If this is not a human player, the dancer is not shown FOREACH_HumanPlayer( pu ) { diff --git a/src/DancingCharacters.cpp b/src/DancingCharacters.cpp index 6653173f9b..8b5be828c3 100644 --- a/src/DancingCharacters.cpp +++ b/src/DancingCharacters.cpp @@ -175,7 +175,7 @@ int Neg1OrPos1() { return RandomInt( 2 ) ? -1 : +1; } void DancingCharacters::Update( float fDelta ) { - if( GAMESTATE->m_bFreeze || GAMESTATE->m_bDelay ) + if( GAMESTATE->m_Position.m_bFreeze || GAMESTATE->m_Position.m_bDelay ) { // spin the camera Matrix-style m_CameraPanYStart += fDelta*40; @@ -184,7 +184,7 @@ void DancingCharacters::Update( float fDelta ) else { // make the characters move - float fBPM = GAMESTATE->m_fCurBPS*60; + float fBPM = GAMESTATE->m_Position.m_fCurBPS*60; float fUpdateScale = SCALE( fBPM, 60.f, 300.f, 0.75f, 1.5f ); CLAMP( fUpdateScale, 0.75f, 1.5f ); @@ -209,8 +209,8 @@ void DancingCharacters::Update( float fDelta ) } bWasGameplayStarting = bGameplayStarting; - static float fLastBeat = GAMESTATE->m_fSongBeat; - float fThisBeat = GAMESTATE->m_fSongBeat; + static float fLastBeat = GAMESTATE->m_Position.m_fSongBeat; + float fThisBeat = GAMESTATE->m_Position.m_fSongBeat; if( fLastBeat < GAMESTATE->m_pCurSong->m_fFirstBeat && fThisBeat >= GAMESTATE->m_pCurSong->m_fFirstBeat ) { @@ -220,7 +220,7 @@ void DancingCharacters::Update( float fDelta ) fLastBeat = fThisBeat; // time for a new sweep? - if( GAMESTATE->m_fSongBeat > m_fThisCameraEndBeat ) + if( GAMESTATE->m_Position.m_fSongBeat > m_fThisCameraEndBeat ) { if( RandomInt(6) >= 4 ) { @@ -248,7 +248,7 @@ void DancingCharacters::Update( float fDelta ) m_fLookAtHeight = CAMERA_STILL_LOOK_AT_HEIGHT; } - int iCurBeat = (int)GAMESTATE->m_fSongBeat; + int iCurBeat = (int)GAMESTATE->m_Position.m_fSongBeat; iCurBeat -= iCurBeat%8; m_fThisCameraStartBeat = (float) iCurBeat; @@ -313,7 +313,7 @@ void DancingCharacters::DrawPrimitives() if(m_fThisCameraStartBeat == m_fThisCameraEndBeat) fPercentIntoSweep = 0; else - fPercentIntoSweep = SCALE(GAMESTATE->m_fSongBeat, m_fThisCameraStartBeat, m_fThisCameraEndBeat, 0.f, 1.f ); + fPercentIntoSweep = SCALE(GAMESTATE->m_Position.m_fSongBeat, m_fThisCameraStartBeat, m_fThisCameraEndBeat, 0.f, 1.f ); float fCameraPanY = SCALE( fPercentIntoSweep, 0.f, 1.f, m_CameraPanYStart, m_CameraPanYEnd ); float fCameraHeight = SCALE( fPercentIntoSweep, 0.f, 1.f, m_fCameraHeightStart, m_fCameraHeightEnd ); diff --git a/src/Foreground.cpp b/src/Foreground.cpp index 708fbcb608..f7b7307b37 100644 --- a/src/Foreground.cpp +++ b/src/Foreground.cpp @@ -41,9 +41,9 @@ void Foreground::LoadFromSong( const Song *pSong ) bga.m_fStartBeat = change.m_fStartBeat; bga.m_bFinished = false; - const float fStartSecond = pSong->m_Timing.GetElapsedTimeFromBeat( bga.m_fStartBeat ); + const float fStartSecond = pSong->m_SongTiming.GetElapsedTimeFromBeat( bga.m_fStartBeat ); const float fStopSecond = fStartSecond + bga.m_bga->GetTweenTimeLeft(); - bga.m_fStopBeat = pSong->m_Timing.GetBeatFromElapsedTime( fStopSecond ); + bga.m_fStopBeat = pSong->m_SongTiming.GetBeatFromElapsedTime( fStopSecond ); bga.m_bga->SetVisible( false ); @@ -65,7 +65,7 @@ void Foreground::Update( float fDeltaTime ) { LoadedBGA &bga = m_BGAnimations[i]; - if( GAMESTATE->m_fSongBeat < bga.m_fStartBeat ) + if( GAMESTATE->m_Position.m_fSongBeat < bga.m_fStartBeat ) { // The animation hasn't started yet. continue; @@ -82,12 +82,12 @@ void Foreground::Update( float fDeltaTime ) bga.m_bga->SetVisible( true ); bga.m_bga->PlayCommand( "On" ); - const float fStartSecond = m_pSong->m_Timing.GetElapsedTimeFromBeat( bga.m_fStartBeat ); - lDeltaTime = GAMESTATE->m_fMusicSeconds - fStartSecond; + const float fStartSecond = m_pSong->m_SongTiming.GetElapsedTimeFromBeat( bga.m_fStartBeat ); + lDeltaTime = GAMESTATE->m_Position.m_fMusicSeconds - fStartSecond; } else { - lDeltaTime = GAMESTATE->m_fMusicSeconds - m_fLastMusicSeconds; + lDeltaTime = GAMESTATE->m_Position.m_fMusicSeconds - m_fLastMusicSeconds; } // This shouldn't go down, but be safe: @@ -95,7 +95,7 @@ void Foreground::Update( float fDeltaTime ) bga.m_bga->Update( lDeltaTime / fRate ); - if( GAMESTATE->m_fSongBeat > bga.m_fStopBeat ) + if( GAMESTATE->m_Position.m_fSongBeat > bga.m_fStopBeat ) { // Finished. bga.m_bga->SetVisible( false ); @@ -104,7 +104,7 @@ void Foreground::Update( float fDeltaTime ) } } - m_fLastMusicSeconds = GAMESTATE->m_fMusicSeconds; + m_fLastMusicSeconds = GAMESTATE->m_Position.m_fMusicSeconds; } /* diff --git a/src/GameSoundManager.cpp b/src/GameSoundManager.cpp index 98ab3cc26c..a9e71fce24 100644 --- a/src/GameSoundManager.cpp +++ b/src/GameSoundManager.cpp @@ -146,7 +146,7 @@ static void StartMusic( MusicToPlay &ToPlay ) SSCLoader::LoadFromSSCFile(ToPlay.m_sTimingFile, song) ) { ToPlay.HasTiming = true; - ToPlay.m_TimingData = song.m_Timing; + ToPlay.m_TimingData = song.m_SongTiming; // get cabinet lights if any Steps *pStepsCabinetLights = SongUtil::GetOneSteps( &song, StepsType_lights_cabinet ); if( pStepsCabinetLights ) @@ -156,7 +156,7 @@ static void StartMusic( MusicToPlay &ToPlay ) SMLoader::LoadFromSMFile(ToPlay.m_sTimingFile, song) ) { ToPlay.HasTiming = true; - ToPlay.m_TimingData = song.m_Timing; + ToPlay.m_TimingData = song.m_SongTiming; // get cabinet lights if any Steps *pStepsCabinetLights = SongUtil::GetOneSteps( &song, StepsType_lights_cabinet ); if( pStepsCabinetLights ) @@ -201,7 +201,7 @@ static void StartMusic( MusicToPlay &ToPlay ) { /* This song has no real timing data. The offset is arbitrary. Change it so * the beat will line up to where we are now, so we don't have to delay. */ - float fDestBeat = fmodfp( GAMESTATE->m_fSongBeatNoOffset, 1 ); + float fDestBeat = fmodfp( GAMESTATE->m_Position.m_fSongBeatNoOffset, 1 ); float fTime = NewMusic->m_NewTiming.GetElapsedTimeFromBeatNoOffset( fDestBeat ); NewMusic->m_NewTiming.m_fBeat0OffsetInSeconds = fTime; @@ -223,7 +223,7 @@ static void StartMusic( MusicToPlay &ToPlay ) * common when starting a precached sound, but our sound isn't, so it'll * probably take a little longer. Nudge the latency up. */ const float fPresumedLatency = SOUNDMAN->GetPlayLatency() + 0.040f; - const float fCurSecond = GAMESTATE->m_fMusicSeconds + fPresumedLatency; + const float fCurSecond = GAMESTATE->m_Position.m_fMusicSeconds + fPresumedLatency; const float fCurBeat = g_Playing->m_Timing.GetBeatFromElapsedTimeNoOffset( fCurSecond ); /* The beat that the new sound will start on. */ @@ -236,9 +236,9 @@ static void StartMusic( MusicToPlay &ToPlay ) const float fSecondToStartOn = g_Playing->m_Timing.GetElapsedTimeFromBeatNoOffset( fCurBeatToStartOn ); const float fMaximumDistance = 2; - const float fDistance = min( fSecondToStartOn - GAMESTATE->m_fMusicSeconds, fMaximumDistance ); + const float fDistance = min( fSecondToStartOn - GAMESTATE->m_Position.m_fMusicSeconds, fMaximumDistance ); - when = GAMESTATE->m_LastBeatUpdate + fDistance; + when = GAMESTATE->m_Position.m_LastBeatUpdate + fDistance; } /* Important: don't hold the mutex while we load and seek the actual sound. */ @@ -552,8 +552,8 @@ void GameSoundManager::Update( float fDeltaTime ) if( !g_Playing->m_Music->IsPlaying() ) { /* There's no song playing. Fake it. */ - CHECKPOINT_M( ssprintf("%f, delta %f", GAMESTATE->m_fMusicSeconds, fDeltaTime) ); - GAMESTATE->UpdateSongPosition( GAMESTATE->m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); + CHECKPOINT_M( ssprintf("%f, delta %f", GAMESTATE->m_Position.m_fMusicSeconds, fDeltaTime) ); + GAMESTATE->UpdateSongPosition( GAMESTATE->m_Position.m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); return; } @@ -570,8 +570,8 @@ void GameSoundManager::Update( float fDeltaTime ) // if( PREFSMAN->m_bLogSkips && !g_Playing->m_bTimingDelayed ) { - const float fExpectedTimePassed = (tm - GAMESTATE->m_LastBeatUpdate) * g_Playing->m_Music->GetPlaybackRate(); - const float fSoundTimePassed = fSeconds - GAMESTATE->m_fMusicSeconds; + const float fExpectedTimePassed = (tm - GAMESTATE->m_Position.m_LastBeatUpdate) * g_Playing->m_Music->GetPlaybackRate(); + const float fSoundTimePassed = fSeconds - GAMESTATE->m_Position.m_fMusicSeconds; const float fDiff = fExpectedTimePassed - fSoundTimePassed; static RString sLastFile = ""; @@ -580,7 +580,7 @@ void GameSoundManager::Update( float fDeltaTime ) /* If fSoundTimePassed < 0, the sound has probably looped. */ if( sLastFile == ThisFile && fSoundTimePassed >= 0 && fabsf(fDiff) > 0.003f ) LOG->Trace("Song position skip in %s: expected %.3f, got %.3f (cur %f, prev %f) (%.3f difference)", - Basename(ThisFile).c_str(), fExpectedTimePassed, fSoundTimePassed, fSeconds, GAMESTATE->m_fMusicSeconds, fDiff ); + Basename(ThisFile).c_str(), fExpectedTimePassed, fSoundTimePassed, fSeconds, GAMESTATE->m_Position.m_fMusicSeconds, fDiff ); sLastFile = ThisFile; } @@ -599,7 +599,7 @@ void GameSoundManager::Update( float fDeltaTime ) { /* We're still waiting for the new sound to start playing, so keep using the * old timing data and fake the time. */ - GAMESTATE->UpdateSongPosition( GAMESTATE->m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); + GAMESTATE->UpdateSongPosition( GAMESTATE->m_Position.m_fMusicSeconds + fDeltaTime, g_Playing->m_Timing ); } else { @@ -614,7 +614,7 @@ void GameSoundManager::Update( float fDeltaTime ) { static int iBeatLastCrossed = 0; - float fSongBeat = GAMESTATE->m_fSongBeat; + float fSongBeat = GAMESTATE->m_Position.m_fSongBeat; int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); iRowNow = max( 0, iRowNow ); @@ -638,7 +638,7 @@ void GameSoundManager::Update( float fDeltaTime ) NoteData &lights = g_Playing->m_Lights; if( lights.GetNumTracks() > 0 ) // lights data was loaded { - const float fSongBeat = GAMESTATE->m_fLightSongBeat; + const float fSongBeat = GAMESTATE->m_Position.m_fLightSongBeat; const int iSongRow = BeatToNoteRowNotRounded( fSongBeat ); static int iRowLastCrossed = 0; diff --git a/src/GameState.cpp b/src/GameState.cpp index bd4188df15..dbca4fc9a3 100644 --- a/src/GameState.cpp +++ b/src/GameState.cpp @@ -127,7 +127,9 @@ GameState::GameState() : m_pEditSourceSteps( Message_EditSourceStepsChanged ), m_stEditSource( Message_EditSourceStepsTypeChanged ), m_iEditCourseEntryIndex( Message_EditCourseEntryIndexChanged ), - m_sEditLocalProfileID( Message_EditLocalProfileIDChanged ) + m_sEditLocalProfileID( Message_EditLocalProfileIDChanged ), + m_bIsUsingStepTiming( true ), + m_bInStepEditor( false ) { g_pImpl = new GameStateImpl; @@ -893,22 +895,15 @@ const float GameState::MUSIC_SECONDS_INVALID = -5000.0f; void GameState::ResetMusicStatistics() { - m_fMusicSeconds = 0; // MUSIC_SECONDS_INVALID; - // todo: move me to FOREACH_EnabledPlayer( p ) after [NUM_PLAYERS]ing - m_fSongBeat = 0; - m_fSongBeatNoOffset = 0; - m_fCurBPS = 10; - //m_bStop = false; - m_bFreeze = false; - m_bDelay = false; - m_iWarpBeginRow = -1; // Set to -1 because some song may want to warp to row 0. -aj - m_fWarpDestination = -1; // Set when a warp is encountered. also see above. -aj - m_fMusicSecondsVisible = 0; - m_fSongBeatVisible = 0; + m_Position.Reset(); + Actor::SetBGMTime( 0, 0, 0, 0 ); FOREACH_PlayerNumber( p ) + { m_pPlayerState[p]->ClearHopoState(); + m_pPlayerState[p]->m_Position.Reset(); + } } void GameState::ResetStageStatistics() @@ -947,57 +942,23 @@ void GameState::ResetStageStatistics() m_iStageSeed = rand(); } -static Preference g_fVisualDelaySeconds( "VisualDelaySeconds", 0.0f ); - -void GameState::UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp ) +void GameState::UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp, bool bUpdatePlayers ) { - if( !timestamp.IsZero() ) - m_LastBeatUpdate = timestamp; - else - m_LastBeatUpdate.Touch(); + + m_Position.UpdateSongPosition( fPositionSeconds, timing, timestamp ); - // xxx testing: only do this on monotune survivor - /* - if( m_pCurSong && m_pCurSong->GetDisplayFullTitle() == "monotune survivor" ) - LOG->Trace( ssprintf("[GameState::UpdateSongPosition] cur BPS = %f, fPositionSeconds = %f",m_fCurBPS,fPositionSeconds) ); - */ - - timing.GetBeatAndBPSFromElapsedTime( fPositionSeconds, m_fSongBeat, m_fCurBPS, m_bFreeze, m_bDelay, m_iWarpBeginRow, m_fWarpDestination ); - // "Crash reason : -243478.890625 -48695.773438" - ASSERT_M( m_fSongBeat > -2000, ssprintf("Song beat %f at %f seconds", m_fSongBeat, fPositionSeconds) ); - - m_fMusicSeconds = fPositionSeconds; - - m_fLightSongBeat = timing.GetBeatFromElapsedTime( fPositionSeconds + g_fLightsAheadSeconds ); - - m_fSongBeatNoOffset = timing.GetBeatFromElapsedTimeNoOffset( fPositionSeconds ); - - m_fMusicSecondsVisible = fPositionSeconds - g_fVisualDelaySeconds.Get(); - float fThrowAway, fThrowAway2; - bool bThrowAway; - int iThrowAway; - timing.GetBeatAndBPSFromElapsedTime( m_fMusicSecondsVisible, m_fSongBeatVisible, fThrowAway, bThrowAway, bThrowAway, iThrowAway, fThrowAway2 ); - - /* - // xxx testing: only do this on monotune survivor - if( m_pCurSong && m_pCurSong->GetDisplayFullTitle() == "monotune survivor" ) + if( bUpdatePlayers ) { - // and only do it in the known negative bpm region. HACKITY HACK - if(m_fSongBeat >= 445.490f && m_fSongBeat <= 453.72f) + FOREACH_EnabledPlayer( pn ) { - LOG->Trace( ssprintf("fPositionSeconds = %f",fPositionSeconds) ); - LOG->Trace( ssprintf("Song beat: %f (%f seconds), BPS = %f (%f BPM)",m_fSongBeat,m_fMusicSecondsVisible,m_fCurBPS,m_fCurBPS*60.0f) ); - //LOG->Trace( ssprintf("Music seconds visible %f = fPositionSeconds %f - g_fVisualDelaySeconds %f", m_fMusicSecondsVisible,fPositionSeconds,g_fVisualDelaySeconds.Get()) ); - } - else if(m_fSongBeat == 445.500f) - { - LOG->Trace( ssprintf("[beat 445.500] fPositionSeconds = %f",fPositionSeconds) ); - LOG->Trace( ssprintf("Song beat: %f (%f seconds), BPS = %f (%f BPM)",m_fSongBeat,m_fMusicSecondsVisible,m_fCurBPS,m_fCurBPS*60.0f) ); + if( m_pCurSteps[pn] ) + { + m_pPlayerState[pn]->m_Position.UpdateSongPosition( fPositionSeconds, m_pCurSteps[pn]->m_Timing, timestamp ); + Actor::SetPlayerBGMBeat( pn, m_pPlayerState[pn]->m_Position.m_fSongBeatVisible, m_pPlayerState[pn]->m_Position.m_fSongBeatNoOffset ); + } } } - */ - - Actor::SetBGMTime( m_fMusicSecondsVisible, m_fSongBeatVisible, fPositionSeconds, m_fSongBeatNoOffset ); + Actor::SetBGMTime( GAMESTATE->m_Position.m_fMusicSecondsVisible, GAMESTATE->m_Position.m_fSongBeatVisible, fPositionSeconds, GAMESTATE->m_Position.m_fSongBeatNoOffset ); // LOG->Trace( "m_fMusicSeconds = %f, m_fSongBeat = %f, m_fCurBPS = %f, m_bFreeze = %f", m_fMusicSeconds, m_fSongBeat, m_fCurBPS, m_bFreeze ); } @@ -2291,11 +2252,16 @@ public: DEFINE_METHOD( GetHardestStepsDifficulty, GetHardestStepsDifficulty() ) DEFINE_METHOD( IsEventMode, IsEventMode() ) DEFINE_METHOD( GetNumPlayersEnabled, GetNumPlayersEnabled() ) - DEFINE_METHOD( GetSongBeat, m_fSongBeat ) - DEFINE_METHOD( GetSongBeatVisible, m_fSongBeatVisible ) - DEFINE_METHOD( GetSongBPS, m_fCurBPS ) - DEFINE_METHOD( GetSongFreeze, m_bFreeze ) - DEFINE_METHOD( GetSongDelay, m_bDelay ) + /*DEFINE_METHOD( GetSongBeat, m_Position.m_fSongBeat ) + DEFINE_METHOD( GetSongBeatVisible, m_Position.m_fSongBeatVisible ) + DEFINE_METHOD( GetSongBPS, m_Position.m_fCurBPS ) + DEFINE_METHOD( GetSongFreeze, m_Position.m_bFreeze ) + DEFINE_METHOD( GetSongDelay, m_Position.m_bDelay )*/ + static int GetSongPosition( T* p, lua_State *L ) + { + p->m_Position.PushSelf(L); + return 1; + } DEFINE_METHOD( GetGameplayLeadIn, m_bGameplayLeadIn ) DEFINE_METHOD( GetCoins, m_iCoins ) DEFINE_METHOD( IsSideJoined, m_bSideIsJoined[Enum::Check(L, 1)] ) @@ -2434,7 +2400,7 @@ public: static int JoinPlayer( T* p, lua_State *L ) { p->JoinPlayer(Enum::Check(L, 1)); return 0; } static int UnjoinPlayer( T* p, lua_State *L ) { p->UnjoinPlayer(Enum::Check(L, 1)); return 0; } static int GetSongPercent( T* p, lua_State *L ) { lua_pushnumber(L, p->GetSongPercent(FArg(1))); return 1; } - DEFINE_METHOD( GetCurMusicSeconds, m_fMusicSeconds ) + DEFINE_METHOD( GetCurMusicSeconds, m_Position.m_fMusicSeconds ) DEFINE_METHOD( GetWorkoutGoalComplete, m_bWorkoutGoalComplete ) static int GetCharacter( T* p, lua_State *L ) { p->m_pCurCharacters[Enum::Check(L, 1)]->PushSelf(L); return 1; } @@ -2500,11 +2466,12 @@ public: ADD_METHOD( GetHardestStepsDifficulty ); ADD_METHOD( IsEventMode ); ADD_METHOD( GetNumPlayersEnabled ); - ADD_METHOD( GetSongBeat ); + /*ADD_METHOD( GetSongBeat ); ADD_METHOD( GetSongBeatVisible ); ADD_METHOD( GetSongBPS ); ADD_METHOD( GetSongFreeze ); - ADD_METHOD( GetSongDelay ); + ADD_METHOD( GetSongDelay );*/ + ADD_METHOD( GetSongPosition ); ADD_METHOD( GetGameplayLeadIn ); ADD_METHOD( GetCoins ); ADD_METHOD( IsSideJoined ); diff --git a/src/GameState.h b/src/GameState.h index 406e4784b1..5e925e05ff 100644 --- a/src/GameState.h +++ b/src/GameState.h @@ -10,6 +10,7 @@ #include "RageTimer.h" #include "PlayerOptions.h" #include "SongOptions.h" +#include "SongPosition.h" #include "Preference.h" #include @@ -43,7 +44,8 @@ public: void ResetPlayer( PlayerNumber pn ); void ApplyCmdline(); // called by Reset void ApplyGameCommand( const RString &sCommand, PlayerNumber pn=PLAYER_INVALID ); - void BeginGame(); // called when first player joins + /** @brief Start the game when the first player joins in. */ + void BeginGame(); void JoinPlayer( PlayerNumber pn ); void UnjoinPlayer( PlayerNumber pn ); bool JoinInput( PlayerNumber pn ); @@ -55,13 +57,25 @@ public: bool HaveProfileToSave(); void SaveLocalData(); void LoadCurrentSettingsFromProfile( PlayerNumber pn ); - void SaveCurrentSettingsToProfile( PlayerNumber pn ); // called at the beginning of each stage + /** + * @brief Save the specified player's settings to his/her profile. + * + * This is called at the beginning of each stage. + * @param pn the PlayerNumber to save the stats to. */ + void SaveCurrentSettingsToProfile( PlayerNumber pn ); Song* GetDefaultSong() const; void Update( float fDelta ); // Main state info - void SetCurGame( const Game *pGame ); // Call this instead of m_pCurGame.Set to make sure PREFSMAN->m_sCurrentGame stays in sync + + /** + * @brief State what the current game is. + * + * Call this instead of m_pCurGame.Set to make sure that + * PREFSMAN->m_sCurrentGame stays in sync. + * @param pGame the game to start using. */ + void SetCurGame( const Game *pGame ); BroadcastOnChangePtr m_pCurGame; BroadcastOnChangePtr m_pCurStyle; /** @brief Determine which side is joined. @@ -129,6 +143,9 @@ public: bool IsCourseMode() const; bool IsBattleMode() const; // not Rave + /** + * @brief Do we show the W1 timing judgment? + * @return true if we do, or false otherwise. */ bool ShowW1() const; BroadcastOnChange m_sPreferredSongGroup; // GROUP_ALL denotes no preferred group @@ -141,7 +158,11 @@ public: SortOrder m_PreferredSortOrder; // used by MusicWheel EditMode m_EditMode; bool IsEditing() const { return m_EditMode != EditMode_Invalid; } - bool m_bDemonstrationOrJukebox; // ScreenGameplay does special stuff when this is true + /** + * @brief Are we in the demonstration or jukebox mode? + * + * ScreenGameplay often does special things when this is set to true. */ + bool m_bDemonstrationOrJukebox; bool m_bJukeboxUsesModifiers; int m_iNumStagesOfThisSong; /** @@ -194,29 +215,9 @@ public: bool m_bBackedOutOfFinalStage; // Music statistics: - // Arcade - the current stage (one song). - // Oni/Endless - a single song in a course. - // Let a lot of classes access this info here so they don't have to keep their own copies. - // todo: [NUM_PLAYERS] this for split bpm lolol -aj - float m_fMusicSeconds; // time into the current song, not scaled by music rate - float m_fSongBeat; - float m_fSongBeatNoOffset; - float m_fCurBPS; - float m_fLightSongBeat; // g_fLightsFalloffSeconds ahead - //bool m_bStop; // in the middle of a stop (freeze or delay) - /** @brief A flag to determine if we're in the middle of a freeze/stop. */ - bool m_bFreeze; - /** @brief A flag to determine if we're in the middle of a delay (Pump style stop). */ - bool m_bDelay; - /** @brief The row used to start a warp. */ - int m_iWarpBeginRow; - /** @brief The beat to warp to afterwards. */ - float m_fWarpDestination; - RageTimer m_LastBeatUpdate; // time of last m_fSongBeat, etc. update - BroadcastOnChange m_bGameplayLeadIn; + SongPosition m_Position; - float m_fMusicSecondsVisible; - float m_fSongBeatVisible; + BroadcastOnChange m_bGameplayLeadIn; // if re-adding noteskin changes in courses, add functions and such here -aj void GetAllUsedNoteSkins( vector &out ) const; @@ -224,7 +225,7 @@ public: static const float MUSIC_SECONDS_INVALID; void ResetMusicStatistics(); // Call this when it's time to play a new song. Clears the values above. - void UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp = RageZeroTimer ); + void UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp = RageZeroTimer, bool bUpdatePlayers = false ); float GetSongPercent( float beat ) const; bool AllAreInDangerOrWorse() const; @@ -259,8 +260,11 @@ public: // Options stuff ModsGroup m_SongOptions; - // True if the current mode has changed the default NoteSkin, such as Edit/Sync Songs does. - // Note: any mode that wants to use it must set it + /** + * @brief Did the current game mode change the default Noteskin? + * + * This is true if it has: see Edit/Sync Songs for a common example. + * Note: any mode that wants to use this must set it explicitly. */ bool m_bDidModeChangeNoteSkin; void GetDefaultPlayerOptions( PlayerOptions &po ); @@ -327,6 +331,17 @@ public: Premium GetPremium() const; // Edit stuff + + /** + * @brief Is the game right now using Song timing or Steps timing? + * + * Different options are available depending on this setting. */ + bool m_bIsUsingStepTiming; + /** + * @brief Are we presently in the Step Editor, where some rules apply differently? + * + * TODO: Find a better way to implement this. */ + bool m_bInStepEditor; BroadcastOnChange m_stEdit; BroadcastOnChange m_cdEdit; BroadcastOnChangePtr m_pEditSourceSteps; diff --git a/src/GameplayAssist.cpp b/src/GameplayAssist.cpp index 7531c77e43..ac9ae328b3 100644 --- a/src/GameplayAssist.cpp +++ b/src/GameplayAssist.cpp @@ -25,9 +25,9 @@ void GameplayAssist::PlayTicks( const NoteData &nd ) * will start coming out the speaker. Compensate for this by boosting fPositionSeconds * ahead. This is just to make sure that we request the sound early enough for it to * come out on time; the actual precise timing is handled by SetStartTime. */ - float fPositionSeconds = GAMESTATE->m_fMusicSeconds; + float fPositionSeconds = GAMESTATE->m_Position.m_fMusicSeconds; fPositionSeconds += SOUNDMAN->GetPlayLatency() + (float)CommonMetrics::TICK_EARLY_SECONDS + 0.250f; - const TimingData &timing = GAMESTATE->m_pCurSong->m_Timing; + const TimingData &timing = GAMESTATE->m_pCurSong->m_SongTiming; const float fSongBeat = timing.GetBeatFromElapsedTimeNoOffset( fPositionSeconds ); const int iSongRow = max( 0, BeatToNoteRowNotRounded( fSongBeat ) ); @@ -47,11 +47,11 @@ void GameplayAssist::PlayTicks( const NoteData &nd ) { const float fTickBeat = NoteRowToBeat( iClapRow ); const float fTickSecond = timing.GetElapsedTimeFromBeatNoOffset( fTickBeat ); - float fSecondsUntil = fTickSecond - GAMESTATE->m_fMusicSeconds; + float fSecondsUntil = fTickSecond - GAMESTATE->m_Position.m_fMusicSeconds; fSecondsUntil /= GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; /* 2x music rate means the time until the tick is halved */ RageSoundParams p; - p.m_StartTime = GAMESTATE->m_LastBeatUpdate + (fSecondsUntil - (float)CommonMetrics::TICK_EARLY_SECONDS); + p.m_StartTime = GAMESTATE->m_Position.m_LastBeatUpdate + (fSecondsUntil - (float)CommonMetrics::TICK_EARLY_SECONDS); m_soundAssistClap.Play( &p ); } } @@ -83,11 +83,11 @@ void GameplayAssist::PlayTicks( const NoteData &nd ) { const float fTickBeat = NoteRowToBeat( iMetronomeRow ); const float fTickSecond = timing.GetElapsedTimeFromBeatNoOffset( fTickBeat ); - float fSecondsUntil = fTickSecond - GAMESTATE->m_fMusicSeconds; + float fSecondsUntil = fTickSecond - GAMESTATE->m_Position.m_fMusicSeconds; fSecondsUntil /= GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; /* 2x music rate means the time until the tick is halved */ RageSoundParams p; - p.m_StartTime = GAMESTATE->m_LastBeatUpdate + (fSecondsUntil - (float)CommonMetrics::TICK_EARLY_SECONDS); + p.m_StartTime = GAMESTATE->m_Position.m_LastBeatUpdate + (fSecondsUntil - (float)CommonMetrics::TICK_EARLY_SECONDS); if( bIsMeasure ) m_soundAssistMetronomeMeasure.Play( &p ); else diff --git a/src/Inventory.cpp b/src/Inventory.cpp index 0c0291696d..3b7eb88b32 100644 --- a/src/Inventory.cpp +++ b/src/Inventory.cpp @@ -121,7 +121,7 @@ void Inventory::Update( float fDelta ) // use items if this player is CPU-controlled if( m_pPlayerState->m_PlayerController != PC_HUMAN && - GAMESTATE->m_fSongBeat < GAMESTATE->m_pCurSong->m_fLastBeat ) + GAMESTATE->m_Position.m_fSongBeat < GAMESTATE->m_pCurSong->m_fLastBeat ) { // every 1 seconds, try to use an item int iLastSecond = (int)(RageTimer::GetTimeSinceStartFast() - fDelta); diff --git a/src/LightsManager.cpp b/src/LightsManager.cpp index b3f05c585b..54e220bf07 100644 --- a/src/LightsManager.cpp +++ b/src/LightsManager.cpp @@ -230,12 +230,12 @@ void LightsManager::Update( float fDeltaTime ) static float fLastBeat; static int iLight; - if( fracf(GAMESTATE->m_fLightSongBeat) < fracf(fLastBeat) ) + if( fracf(GAMESTATE->m_Position.m_fLightSongBeat) < fracf(fLastBeat) ) { ++iLight; wrap( iLight, 4 ); } - fLastBeat = GAMESTATE->m_fLightSongBeat; + fLastBeat = GAMESTATE->m_Position.m_fLightSongBeat; switch( iLight ) { case 0: m_LightsState.m_bCabinetLights[LIGHT_MARQUEE_UP_LEFT] = true; break; @@ -405,7 +405,7 @@ void LightsManager::Update( float fDeltaTime ) // If not joined, has enough credits, and not too late to join, then // blink the menu buttons rapidly so they'll press Start { - int iBeat = (int)(GAMESTATE->m_fLightSongBeat*4); + int iBeat = (int)(GAMESTATE->m_Position.m_fLightSongBeat*4); bool bBlinkOn = (iBeat%2)==0; FOREACH_PlayerNumber( pn ) { diff --git a/src/LyricDisplay.cpp b/src/LyricDisplay.cpp index 27263d068c..7284d2dc17 100644 --- a/src/LyricDisplay.cpp +++ b/src/LyricDisplay.cpp @@ -41,9 +41,9 @@ void LyricDisplay::Update( float fDeltaTime ) return; // If the song has changed (in a course), reset. - if( GAMESTATE->m_fMusicSeconds < m_fLastSecond ) + if( GAMESTATE->m_Position.m_fMusicSeconds < m_fLastSecond ) Init(); - m_fLastSecond = GAMESTATE->m_fMusicSeconds; + m_fLastSecond = GAMESTATE->m_Position.m_fMusicSeconds; if( m_iCurLyricNumber >= GAMESTATE->m_pCurSong->m_LyricSegments.size() ) return; @@ -51,7 +51,7 @@ void LyricDisplay::Update( float fDeltaTime ) const Song *pSong = GAMESTATE->m_pCurSong; const float fStartTime = (pSong->m_LyricSegments[m_iCurLyricNumber].m_fStartTime) - IN_LENGTH.GetValue(); - if( GAMESTATE->m_fMusicSeconds < fStartTime ) + if( GAMESTATE->m_Position.m_fMusicSeconds < fStartTime ) return; // Clamp this lyric to the beginning of the next or the end of the music. @@ -59,7 +59,7 @@ void LyricDisplay::Update( float fDeltaTime ) if( m_iCurLyricNumber+1 < GAMESTATE->m_pCurSong->m_LyricSegments.size() ) fEndTime = pSong->m_LyricSegments[m_iCurLyricNumber+1].m_fStartTime; else - fEndTime = pSong->GetElapsedTimeFromBeat( pSong->m_fLastBeat ); + fEndTime = pSong->m_SongTiming.GetElapsedTimeFromBeat( pSong->m_fLastBeat ); const float fDistance = fEndTime - pSong->m_LyricSegments[m_iCurLyricNumber].m_fStartTime; const float fTweenBufferTime = IN_LENGTH.GetValue() + OUT_LENGTH.GetValue(); diff --git a/src/Makefile.am b/src/Makefile.am index 2a60163c84..178a263f39 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -373,7 +373,8 @@ ScoreDisplayBattle.cpp ScoreDisplayBattle.h \ ScoreDisplayCalories.cpp ScoreDisplayCalories.h \ ScoreDisplayLifeTime.cpp ScoreDisplayLifeTime.h \ ScoreDisplayNormal.cpp ScoreDisplayNormal.h ScoreDisplayOni.cpp ScoreDisplayOni.h \ -ScoreDisplayPercentage.cpp ScoreDisplayPercentage.h ScoreDisplayRave.cpp ScoreDisplayRave.h +ScoreDisplayPercentage.cpp ScoreDisplayPercentage.h ScoreDisplayRave.cpp ScoreDisplayRave.h \ +SongPosition.cpp SongPosition.h PCRE = ../extern/pcre/get.c ../extern/pcre/internal.h ../extern/pcre/maketables.c ../extern/pcre/pcre.c ../extern/pcre/pcre.h ../extern/pcre/study.c EXTRA_DIST += ../extern/pcre/chartables.c diff --git a/src/MeterDisplay.cpp b/src/MeterDisplay.cpp index 3018125cd1..641b50d73c 100644 --- a/src/MeterDisplay.cpp +++ b/src/MeterDisplay.cpp @@ -73,9 +73,9 @@ void SongMeterDisplay::Update( float fDeltaTime ) { if( GAMESTATE->m_pCurSong ) { - float fSongStartSeconds = GAMESTATE->m_pCurSong->m_Timing.GetElapsedTimeFromBeat( GAMESTATE->m_pCurSong->m_fFirstBeat ); - float fSongEndSeconds = GAMESTATE->m_pCurSong->m_Timing.GetElapsedTimeFromBeat( GAMESTATE->m_pCurSong->m_fLastBeat ); - float fPercentPositionSong = SCALE( GAMESTATE->m_fMusicSeconds, fSongStartSeconds, fSongEndSeconds, 0.0f, 1.0f ); + float fSongStartSeconds = GAMESTATE->m_pCurSong->m_SongTiming.GetElapsedTimeFromBeat( GAMESTATE->m_pCurSong->m_fFirstBeat ); + float fSongEndSeconds = GAMESTATE->m_pCurSong->m_SongTiming.GetElapsedTimeFromBeat( GAMESTATE->m_pCurSong->m_fLastBeat ); + float fPercentPositionSong = SCALE( GAMESTATE->m_Position.m_fMusicSeconds, fSongStartSeconds, fSongEndSeconds, 0.0f, 1.0f ); CLAMP( fPercentPositionSong, 0, 1 ); SetPercent( fPercentPositionSong ); diff --git a/src/NoteDataUtil.cpp b/src/NoteDataUtil.cpp index 6b7f47cba1..413fd3a425 100644 --- a/src/NoteDataUtil.cpp +++ b/src/NoteDataUtil.cpp @@ -2215,7 +2215,7 @@ void NoteDataUtil::AddTapAttacks( NoteData &nd, Song* pSong ) for( float sec=15; secm_fMusicLengthSeconds; sec+=30 ) { - float fBeat = pSong->GetBeatFromElapsedTime( sec ); + float fBeat = pSong->m_SongTiming.GetBeatFromElapsedTime( sec ); int iBeat = (int)fBeat; int iTrack = iBeat % nd.GetNumTracks(); // deterministically calculates track TapNote tn( @@ -2448,7 +2448,7 @@ void NoteDataUtil::SetHopoPossibleFlags( const Song *pSong, NoteData& ndInOut ) FOREACH_NONEMPTY_ROW_ALL_TRACKS( ndInOut, r ) { float fBeat = NoteRowToBeat( r ); - float fSeconds = pSong->GetElapsedTimeFromBeat( fBeat ); + float fSeconds = pSong->m_SongTiming.GetElapsedTimeFromBeat( fBeat ); int iLastTapTrack = ndInOut.GetLastTrackWithTapOrHoldHead( r ); if( iLastTapTrack != -1 && fSeconds <= fLastRowMusicSeconds + g_fTimingWindowHopo ) diff --git a/src/NoteDisplay.cpp b/src/NoteDisplay.cpp index 3998c1fb08..a5c530bd73 100644 --- a/src/NoteDisplay.cpp +++ b/src/NoteDisplay.cpp @@ -286,7 +286,7 @@ void NoteDisplay::Update( float fDeltaTime ) void NoteDisplay::SetActiveFrame( float fNoteBeat, Actor &actorToSet, float fAnimationLength, bool bVivid ) { /* -inf ... inf */ - float fBeatOrSecond = cache->m_bAnimationBasedOnBeats ? GAMESTATE->m_fSongBeat : GAMESTATE->m_fMusicSeconds; + float fBeatOrSecond = cache->m_bAnimationBasedOnBeats ? m_pPlayerState->m_Position.m_fSongBeat : m_pPlayerState->m_Position.m_fMusicSeconds; /* -len ... +len */ float fPercentIntoAnimation = fmodf( fBeatOrSecond, fAnimationLength ); /* -1 ... 1 */ diff --git a/src/NoteField.cpp b/src/NoteField.cpp index 6895b07234..cdf67941c8 100644 --- a/src/NoteField.cpp +++ b/src/NoteField.cpp @@ -32,6 +32,33 @@ static ThemeMetric FADE_FAIL_TIME( "NoteField", "FadeFailTime" ); static RString RoutineNoteSkinName( size_t i ) { return ssprintf("RoutineNoteSkinP%i",int(i+1)); } static ThemeMetric1D ROUTINE_NOTESKIN( "NoteField", RoutineNoteSkinName, NUM_PLAYERS ); + +inline const TimingData *GetRealTiming(const PlayerState *pPlayerState) +{ + if( GAMESTATE->m_pCurSteps[pPlayerState->m_PlayerNumber] != NULL ) + return &GAMESTATE->m_pCurSteps[pPlayerState->m_PlayerNumber]->m_Timing; + return NULL; +} + +inline const TimingData *GetDisplayedTiming(const PlayerState *pPlayerState) +{ + if( !GAMESTATE->m_bIsUsingStepTiming ) + return &GAMESTATE->m_pCurSong->m_SongTiming; + return GetRealTiming(pPlayerState); +} + +inline const SongPosition *GetRealPosition(const PlayerState *pPlayerState) +{ + return &pPlayerState->m_Position; +} + +inline const SongPosition *GetDisplayedPosition(const PlayerState *pPlayerState) +{ + if( !GAMESTATE->m_bIsUsingStepTiming ) + return &GAMESTATE->m_Position; + return GetRealPosition(pPlayerState); +} + NoteField::NoteField() { m_pNoteData = NULL; @@ -240,7 +267,7 @@ void NoteField::Update( float fDeltaTime ) ActorFrame::Update( fDeltaTime ); // update m_fBoardOffsetPixels, m_fCurrentBeatLastUpdate, m_fYPosCurrentBeatLastUpdate - const float fCurrentBeat = GAMESTATE->m_fSongBeat; + const float fCurrentBeat = GetDisplayedPosition(m_pPlayerState)->m_fSongBeat; bool bTweeningOn = m_sprBoard->GetCurrentDiffuseAlpha() >= 0.98 && m_sprBoard->GetCurrentDiffuseAlpha() < 1.00; // HACK if( !bTweeningOn && m_fCurrentBeatLastUpdate != -1 ) { @@ -427,6 +454,8 @@ static ThemeMetric TIME_SIGNATURE_COLOR ( "NoteField", "TimeSignature static ThemeMetric TICKCOUNT_COLOR ( "NoteField", "TickcountColor" ); static ThemeMetric COMBO_COLOR ( "NoteField", "ComboColor" ); static ThemeMetric LABEL_COLOR ( "NoteField", "LabelColor" ); +static ThemeMetric SPEED_COLOR ( "NoteField", "SpeedColor" ); +static ThemeMetric FAKE_COLOR ("NoteField", "FakeColor" ); static ThemeMetric BPM_IS_LEFT_SIDE ( "NoteField", "BPMIsLeftSide" ); static ThemeMetric STOP_IS_LEFT_SIDE ( "NoteField", "StopIsLeftSide" ); static ThemeMetric DELAY_IS_LEFT_SIDE ( "NoteField", "DelayIsLeftSide" ); @@ -435,6 +464,8 @@ static ThemeMetric TIME_SIGNATURE_IS_LEFT_SIDE ( "NoteField", "TimeSignatu static ThemeMetric TICKCOUNT_IS_LEFT_SIDE ( "NoteField", "TickcountIsLeftSide" ); static ThemeMetric COMBO_IS_LEFT_SIDE ( "NoteField", "ComboIsLeftSide" ); static ThemeMetric LABEL_IS_LEFT_SIDE ( "NoteField", "LabelIsLeftSide" ); +static ThemeMetric SPEED_IS_LEFT_SIDE ( "NoteField", "SpeedIsLeftSide" ); +static ThemeMetric FAKE_IS_LEFT_SIDE ( "NoteField", "FakeIsLeftSide" ); static ThemeMetric BPM_OFFSETX ( "NoteField", "BPMOffsetX" ); static ThemeMetric STOP_OFFSETX ( "NoteField", "StopOffsetX" ); static ThemeMetric DELAY_OFFSETX ( "NoteField", "DelayOffsetX" ); @@ -443,6 +474,8 @@ static ThemeMetric TIME_SIGNATURE_OFFSETX ( "NoteField", "TimeSignatureOf static ThemeMetric TICKCOUNT_OFFSETX ( "NoteField", "TickcountOffsetX" ); static ThemeMetric COMBO_OFFSETX ( "NoteField", "ComboOffsetX" ); static ThemeMetric LABEL_OFFSETX ( "NoteField", "LabelOffsetX" ); +static ThemeMetric SPEED_OFFSETX ( "NoteField", "SpeedOffsetX" ); +static ThemeMetric FAKE_OFFSETX ( "NoteField", "FakeOffsetX" ); void NoteField::DrawBPMText( const float fBeat, const float fBPM ) { @@ -572,6 +605,40 @@ void NoteField::DrawLabelText( const float fBeat, RString sLabel ) m_textMeasureNumber.Draw(); } +void NoteField::DrawSpeedText( const float fBeat, float fPercent, float fWait, unsigned short usMode ) +{ + const float fYOffset = ArrowEffects::GetYOffset( m_pPlayerState, 0, fBeat ); + const float fYPos = ArrowEffects::GetYPos( m_pPlayerState, 0, fYOffset, m_fYReverseOffsetPixels ); + const float fZoom = ArrowEffects::GetZoom( m_pPlayerState ); + const float xBase = GetWidth()/2.f; + const float xOffset = SPEED_OFFSETX * fZoom; + + m_textMeasureNumber.SetZoom( fZoom ); + m_textMeasureNumber.SetHorizAlign( SPEED_IS_LEFT_SIDE ? align_right : align_left ); + m_textMeasureNumber.SetDiffuse( SPEED_COLOR ); + m_textMeasureNumber.SetGlow( RageColor(1,1,1,RageFastCos(RageTimer::GetTimeSinceStartFast()*2)/2+0.5f) ); + m_textMeasureNumber.SetText( ssprintf("%.3f\n%s\n%.3f", fPercent, (usMode == 1 ? "S" : "B"), fWait) ); + m_textMeasureNumber.SetXY( (SPEED_IS_LEFT_SIDE ? -xBase - xOffset : xBase + xOffset), fYPos ); + m_textMeasureNumber.Draw(); +} + +void NoteField::DrawFakeText( const float fBeat, const float fNewBeat ) +{ + const float fYOffset = ArrowEffects::GetYOffset( m_pPlayerState, 0, fBeat ); + const float fYPos = ArrowEffects::GetYPos( m_pPlayerState, 0, fYOffset, m_fYReverseOffsetPixels ); + const float fZoom = ArrowEffects::GetZoom( m_pPlayerState ); + const float xBase = GetWidth()/2.f; + const float xOffset = FAKE_OFFSETX * fZoom; + + m_textMeasureNumber.SetZoom( fZoom ); + m_textMeasureNumber.SetHorizAlign( FAKE_IS_LEFT_SIDE ? align_right : align_left ); + m_textMeasureNumber.SetDiffuse( FAKE_COLOR ); + m_textMeasureNumber.SetGlow( RageColor(1,1,1,RageFastCos(RageTimer::GetTimeSinceStartFast()*2)/2+0.5f) ); + m_textMeasureNumber.SetText( ssprintf("%.3f", fNewBeat) ); + m_textMeasureNumber.SetXY( (FAKE_IS_LEFT_SIDE ? -xBase - xOffset : xBase + xOffset), fYPos ); + m_textMeasureNumber.Draw(); +} + void NoteField::DrawAttackText( const float fBeat, const Attack &attack ) { const float fYOffset = ArrowEffects::GetYOffset( m_pPlayerState, 0, fBeat ); @@ -606,7 +673,7 @@ void NoteField::DrawBGChangeText( const float fBeat, const RString sNewBGName ) // change this probing to binary search float FindFirstDisplayedBeat( const PlayerState* pPlayerState, int iDrawDistanceAfterTargetsPixels ) { - float fFirstBeatToDraw = GAMESTATE->m_fSongBeat-4; // Adjust to balance off performance and showing enough notes. + float fFirstBeatToDraw = GetDisplayedPosition(pPlayerState)->m_fSongBeat-4; // Adjust to balance off performance and showing enough notes. /* In Boomerang, we'll usually have two sections of notes: before and after * the peak. We always start drawing before the peak, and end after it, or @@ -618,7 +685,7 @@ float FindFirstDisplayedBeat( const PlayerState* pPlayerState, int iDrawDistance bBoomerang = (fAccels[PlayerOptions::ACCEL_BOOMERANG] != 0); } - while( fFirstBeatToDraw < GAMESTATE->m_fSongBeat ) + while( fFirstBeatToDraw < GetDisplayedPosition(pPlayerState)->m_fSongBeat ) { bool bIsPastPeakYOffset; float fPeakYOffset; @@ -640,7 +707,7 @@ float FindLastDisplayedBeat( const PlayerState* pPlayerState, int iDrawDistanceB // Probe for last note to draw. Worst case is 0.25x + boost. // Adjust search distance so that notes don't pop onto the screen. float fSearchDistance = 10; - float fLastBeatToDraw = GAMESTATE->m_fSongBeat+fSearchDistance; + float fLastBeatToDraw = GetDisplayedPosition(pPlayerState)->m_fSongBeat+fSearchDistance; const int NUM_ITERATIONS = 20; @@ -669,6 +736,19 @@ float FindLastDisplayedBeat( const PlayerState* pPlayerState, int iDrawDistanceB return fLastBeatToDraw; } +inline float NoteRowToVisibleBeat( const PlayerState *pPlayerState, int iRow ) +{ + /* + if( GAMESTATE->m_bIsUsingStepTiming ) + { + */ + return NoteRowToBeat(iRow); + /* + } + return GetDisplayedTiming(pPlayerState)->GetBeatFromElapsedTime(GetRealTiming(pPlayerState)->GetElapsedTimeFromBeat(NoteRowToBeat(iRow))); + */ +} + bool NoteField::IsOnScreen( float fBeat, int iCol, int iDrawDistanceAfterTargetsPixels, int iDrawDistanceBeforeTargetsPixels ) const { // TRICKY: If boomerang is on, then ones in the range @@ -739,10 +819,13 @@ void NoteField::DrawPrimitives() cur->m_ReceptorArrowRow.Draw(); } + const TimingData *pTiming = GetDisplayedTiming(m_pPlayerState); + // Draw beat bars - if( GAMESTATE->IsEditing() || SHOW_BEAT_BARS ) + if( ( GAMESTATE->IsEditing() || SHOW_BEAT_BARS ) && pTiming != NULL ) { - const vector &vTimeSignatureSegments = GAMESTATE->m_pCurSong->m_Timing.m_vTimeSignatureSegments; + const TimingData &timing = *pTiming; + const vector &vTimeSignatureSegments = timing.m_vTimeSignatureSegments; int iMeasureIndex = 0; FOREACH_CONST( TimeSignatureSegment, vTimeSignatureSegments, iter ) { @@ -781,12 +864,14 @@ void NoteField::DrawPrimitives() } } - if( GAMESTATE->IsEditing() ) + if( GAMESTATE->IsEditing() && pTiming != NULL ) { ASSERT(GAMESTATE->m_pCurSong); + const TimingData &timing = *pTiming; + // BPM text - FOREACH_CONST( BPMSegment, GAMESTATE->m_pCurSong->m_Timing.m_BPMSegments, seg ) + FOREACH_CONST( BPMSegment, timing.m_BPMSegments, seg ) { if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) { @@ -797,7 +882,7 @@ void NoteField::DrawPrimitives() } // Freeze text - FOREACH_CONST( StopSegment, GAMESTATE->m_pCurSong->m_Timing.m_StopSegments, seg ) + FOREACH_CONST( StopSegment, timing.m_StopSegments, seg ) { if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) { @@ -808,18 +893,21 @@ void NoteField::DrawPrimitives() } // Warp text - FOREACH_CONST( WarpSegment, GAMESTATE->m_pCurSong->m_Timing.m_WarpSegments, seg ) + if( GAMESTATE->m_bIsUsingStepTiming ) { - if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + FOREACH_CONST( WarpSegment, timing.m_WarpSegments, seg ) { - float fBeat = NoteRowToBeat(seg->m_iStartRow); - if( IS_ON_SCREEN(fBeat) ) - DrawWarpText( fBeat, seg->m_fEndBeat ); + if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + { + float fBeat = NoteRowToBeat(seg->m_iStartRow); + if( IS_ON_SCREEN(fBeat) ) + DrawWarpText( fBeat, seg->m_fLengthBeats ); + } } } - + // Time Signature text - FOREACH_CONST( TimeSignatureSegment, GAMESTATE->m_pCurSong->m_Timing.m_vTimeSignatureSegments, seg ) + FOREACH_CONST( TimeSignatureSegment, timing.m_vTimeSignatureSegments, seg ) { if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) { @@ -829,30 +917,36 @@ void NoteField::DrawPrimitives() } } - // Tickcount text - FOREACH_CONST( TickcountSegment, GAMESTATE->m_pCurSong->m_Timing.m_TickcountSegments, seg ) + if( GAMESTATE->m_bIsUsingStepTiming ) { - if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + // Tickcount text + FOREACH_CONST( TickcountSegment, timing.m_TickcountSegments, seg ) { - float fBeat = NoteRowToBeat(seg->m_iStartRow); - if( IS_ON_SCREEN(fBeat) ) - DrawTickcountText( fBeat, seg->m_iTicks ); + if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + { + float fBeat = NoteRowToBeat(seg->m_iStartRow); + if( IS_ON_SCREEN(fBeat) ) + DrawTickcountText( fBeat, seg->m_iTicks ); + } } } - // Combo text - FOREACH_CONST( ComboSegment, GAMESTATE->m_pCurSong->m_Timing.m_ComboSegments, seg ) + if( GAMESTATE->m_bIsUsingStepTiming ) { - if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + // Combo text + FOREACH_CONST( ComboSegment, timing.m_ComboSegments, seg ) { - float fBeat = NoteRowToBeat(seg->m_iStartRow); - if( IS_ON_SCREEN(fBeat) ) - DrawComboText( fBeat, seg->m_iCombo ); + if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + { + float fBeat = NoteRowToBeat(seg->m_iStartRow); + if( IS_ON_SCREEN(fBeat) ) + DrawComboText( fBeat, seg->m_iCombo ); + } } } // Label text - FOREACH_CONST( LabelSegment, GAMESTATE->m_pCurSong->m_Timing.m_LabelSegments, seg ) + FOREACH_CONST( LabelSegment, timing.m_LabelSegments, seg ) { if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) { @@ -861,6 +955,33 @@ void NoteField::DrawPrimitives() DrawLabelText( fBeat, seg->m_sLabel ); } } + + if( GAMESTATE->m_bIsUsingStepTiming ) + { + FOREACH_CONST( SpeedSegment, timing.m_SpeedSegments, seg ) + { + if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + { + float fBeat = NoteRowToBeat(seg->m_iStartRow); + if( IS_ON_SCREEN(fBeat) ) + DrawSpeedText( fBeat, seg->m_fPercent, seg->m_fWait, seg->m_usMode ); + } + } + } + + // Speed text + if( GAMESTATE->m_bIsUsingStepTiming ) + { + FOREACH_CONST( FakeSegment, timing.m_FakeSegments, seg ) + { + if( seg->m_iStartRow >= iFirstRowToDraw && seg->m_iStartRow <= iLastRowToDraw ) + { + float fBeat = NoteRowToBeat(seg->m_iStartRow); + if( IS_ON_SCREEN(fBeat) ) + DrawFakeText( fBeat, seg->m_fLengthBeats ); + } + } + } // Course mods text const Course *pCourse = GAMESTATE->m_pCurCourse; @@ -872,7 +993,7 @@ void NoteField::DrawPrimitives() FOREACH_CONST( Attack, ce.attacks, a ) { float fSecond = a->fStartSecond; - float fBeat = GAMESTATE->m_pCurSong->m_Timing.GetBeatFromElapsedTime( fSecond ); + float fBeat = timing.GetBeatFromElapsedTime( fSecond ); if( BeatToNoteRow(fBeat) >= iFirstRowToDraw && BeatToNoteRow(fBeat) <= iLastRowToDraw) @@ -882,71 +1003,74 @@ void NoteField::DrawPrimitives() } } } - - // BGChange text - switch( GAMESTATE->m_EditMode ) + + if( !GAMESTATE->m_bIsUsingStepTiming ) { - case EditMode_Home: - case EditMode_CourseMods: - case EditMode_Practice: - break; - case EditMode_Full: - { - vector::iterator iter[NUM_BackgroundLayer]; - FOREACH_BackgroundLayer( i ) - iter[i] = GAMESTATE->m_pCurSong->GetBackgroundChanges(i).begin(); - - while( 1 ) + // BGChange text + switch( GAMESTATE->m_EditMode ) + { + case EditMode_Home: + case EditMode_CourseMods: + case EditMode_Practice: + break; + case EditMode_Full: { - float fLowestBeat = FLT_MAX; - vector viLowestIndex; - + vector::iterator iter[NUM_BackgroundLayer]; FOREACH_BackgroundLayer( i ) + iter[i] = GAMESTATE->m_pCurSong->GetBackgroundChanges(i).begin(); + + while( 1 ) { - if( iter[i] == GAMESTATE->m_pCurSong->GetBackgroundChanges(i).end() ) - continue; - - float fBeat = iter[i]->m_fStartBeat; - if( fBeat < fLowestBeat ) - { - fLowestBeat = fBeat; - viLowestIndex.clear(); - viLowestIndex.push_back( i ); - } - else if( fBeat == fLowestBeat ) - { - viLowestIndex.push_back( i ); - } - } - - if( viLowestIndex.empty() ) - { + float fLowestBeat = FLT_MAX; + vector viLowestIndex; + FOREACH_BackgroundLayer( i ) - ASSERT( iter[i] == GAMESTATE->m_pCurSong->GetBackgroundChanges(i).end() ); - break; - } - - if( IS_ON_SCREEN(fLowestBeat) ) - { - vector vsBGChanges; - FOREACH_CONST( BackgroundLayer, viLowestIndex, i ) { - ASSERT( iter[*i] != GAMESTATE->m_pCurSong->GetBackgroundChanges(*i).end() ); - const BackgroundChange& change = *iter[*i]; - RString s = change.GetTextDescription(); - if( *i!=0 ) - s = ssprintf("%d: ",*i) + s; - vsBGChanges.push_back( s ); + if( iter[i] == GAMESTATE->m_pCurSong->GetBackgroundChanges(i).end() ) + continue; + + float fBeat = iter[i]->m_fStartBeat; + if( fBeat < fLowestBeat ) + { + fLowestBeat = fBeat; + viLowestIndex.clear(); + viLowestIndex.push_back( i ); + } + else if( fBeat == fLowestBeat ) + { + viLowestIndex.push_back( i ); + } } - DrawBGChangeText( fLowestBeat, join("\n",vsBGChanges) ); + + if( viLowestIndex.empty() ) + { + FOREACH_BackgroundLayer( i ) + ASSERT( iter[i] == GAMESTATE->m_pCurSong->GetBackgroundChanges(i).end() ); + break; + } + + if( IS_ON_SCREEN(fLowestBeat) ) + { + vector vsBGChanges; + FOREACH_CONST( BackgroundLayer, viLowestIndex, i ) + { + ASSERT( iter[*i] != GAMESTATE->m_pCurSong->GetBackgroundChanges(*i).end() ); + const BackgroundChange& change = *iter[*i]; + RString s = change.GetTextDescription(); + if( *i!=0 ) + s = ssprintf("%d: ",*i) + s; + vsBGChanges.push_back( s ); + } + DrawBGChangeText( fLowestBeat, join("\n",vsBGChanges) ); + } + FOREACH_CONST( BackgroundLayer, viLowestIndex, i ) + iter[*i]++; } - FOREACH_CONST( BackgroundLayer, viLowestIndex, i ) - iter[*i]++; } - } - break; - default: - ASSERT(0); + break; + default: + ASSERT(0); + } } // Draw marker bars @@ -1012,8 +1136,8 @@ void NoteField::DrawPrimitives() float fThrowAway; bool bStartIsPastPeak = false; bool bEndIsPastPeak = false; - float fStartYOffset = ArrowEffects::GetYOffset( m_pPlayerState, c, NoteRowToBeat(iStartRow), fThrowAway, bStartIsPastPeak ); - float fEndYOffset = ArrowEffects::GetYOffset( m_pPlayerState, c, NoteRowToBeat(iEndRow), fThrowAway, bEndIsPastPeak ); + float fStartYOffset = ArrowEffects::GetYOffset( m_pPlayerState, c, NoteRowToVisibleBeat(m_pPlayerState, iStartRow), fThrowAway, bStartIsPastPeak ); + float fEndYOffset = ArrowEffects::GetYOffset( m_pPlayerState, c, NoteRowToVisibleBeat(m_pPlayerState, iEndRow), fThrowAway, bEndIsPastPeak ); bool bTailIsOnVisible = iDrawDistanceAfterTargetsPixels <= fEndYOffset && fEndYOffset <= iDrawDistanceBeforeTargetsPixels; bool bHeadIsVisible = iDrawDistanceAfterTargetsPixels <= fStartYOffset && fStartYOffset <= iDrawDistanceBeforeTargetsPixels; @@ -1043,7 +1167,7 @@ void NoteField::DrawPrimitives() displayCols->display[c].DrawHold( tn, c, iStartRow, bIsHoldingNote, Result, bUseAdditionColoring, bIsInSelectionRange ? fSelectedRangeGlow : m_fPercentFadeToFail, m_fYReverseOffsetPixels, (float) iDrawDistanceAfterTargetsPixels, (float) iDrawDistanceBeforeTargetsPixels, iDrawDistanceBeforeTargetsPixels, FADE_BEFORE_TARGETS_PERCENT ); - bool bNoteIsUpcoming = NoteRowToBeat(iStartRow) > GAMESTATE->m_fSongBeat; + bool bNoteIsUpcoming = NoteRowToBeat(iStartRow) > GetDisplayedPosition(m_pPlayerState)->m_fSongBeat; bAnyUpcomingInThisCol |= bNoteIsUpcoming; } } @@ -1085,7 +1209,7 @@ void NoteField::DrawPrimitives() continue; // skip ASSERT_M( NoteRowToBeat(q) > -2000, ssprintf("%i %i %i, %f %f", q, iLastRowToDraw, - iFirstRowToDraw, GAMESTATE->m_fSongBeat, GAMESTATE->m_fMusicSeconds) ); + iFirstRowToDraw, GetDisplayedPosition(m_pPlayerState)->m_fSongBeat, GetDisplayedPosition(m_pPlayerState)->m_fMusicSeconds) ); // See if there is a hold step that begins on this index. // Only do this if the noteskin cares. @@ -1110,12 +1234,12 @@ void NoteField::DrawPrimitives() bool bIsHopoPossible = (tn.bHopoPossible); bool bUseAdditionColoring = bIsAddition || bIsHopoPossible; NoteDisplayCols *displayCols = tn.pn == PLAYER_INVALID ? m_pCurDisplay : m_pDisplays[tn.pn]; - displayCols->display[c].DrawTap( tn, c, NoteRowToBeat(q), bHoldNoteBeginsOnThisBeat, + displayCols->display[c].DrawTap( tn, c, NoteRowToVisibleBeat(m_pPlayerState, q), bHoldNoteBeginsOnThisBeat, bUseAdditionColoring, bIsInSelectionRange ? fSelectedRangeGlow : m_fPercentFadeToFail, m_fYReverseOffsetPixels, iDrawDistanceAfterTargetsPixels, iDrawDistanceBeforeTargetsPixels, FADE_BEFORE_TARGETS_PERCENT ); - bool bNoteIsUpcoming = NoteRowToBeat(q) > GAMESTATE->m_fSongBeat; + bool bNoteIsUpcoming = NoteRowToBeat(q) > GetDisplayedPosition(m_pPlayerState)->m_fSongBeat; bAnyUpcomingInThisCol |= bNoteIsUpcoming; } diff --git a/src/NoteField.h b/src/NoteField.h index 560f3cb563..a4193f41a5 100644 --- a/src/NoteField.h +++ b/src/NoteField.h @@ -1,6 +1,8 @@ #ifndef NOTE_FIELD_H #define NOTE_FIELD_H +#include "TimingData.h" +#include "SongPosition.h" #include "Sprite.h" #include "ActorFrame.h" #include "BitmapText.h" @@ -62,10 +64,12 @@ protected: void DrawTickcountText( const float fBeat, int iTicks ); void DrawComboText( const float fBeat, int iCombo ); void DrawLabelText( const float fBeat, RString sLabel ); + void DrawSpeedText( const float fBeat, float fPercent, float fWait, unsigned short usMode ); + void DrawFakeText( const float fBeat, const float fNewBeat ); void DrawAttackText( const float fBeat, const Attack &attack ); void DrawBGChangeText( const float fBeat, const RString sNewBGName ); float GetWidth() const; - + const NoteData *m_pNoteData; float m_fPercentFadeToFail; // -1 if not fading to fail diff --git a/src/NotesLoader.cpp b/src/NotesLoader.cpp index 6ed491df63..05112c51e0 100644 --- a/src/NotesLoader.cpp +++ b/src/NotesLoader.cpp @@ -43,11 +43,9 @@ bool NotesLoader::LoadFromDir( const RString &sPath, Song &out, set &Bl SMLoader::GetApplicableFiles( sPath, list ); if (!list.empty() ) return SMLoader::LoadFromDir( sPath, out ); -#if defined(_MSC_VER) || defined(MACOSX) SMALoader::GetApplicableFiles( sPath, list ); if (!list.empty() ) return SMALoader::LoadFromDir( sPath, out ); -#endif DWILoader::GetApplicableFiles( sPath, list ); if( !list.empty() ) return DWILoader::LoadFromDir( sPath, out, BlacklistedImages ); diff --git a/src/NotesLoaderBMS.cpp b/src/NotesLoaderBMS.cpp index a29c833a11..78964895b5 100644 --- a/src/NotesLoaderBMS.cpp +++ b/src/NotesLoaderBMS.cpp @@ -435,9 +435,121 @@ static void ReadTimeSigs( const NameToData_t &mapNameToData, MeasureToTimeSig_t static const int BEATS_PER_MEASURE = 4; static const int ROWS_PER_MEASURE = ROWS_PER_BEAT * BEATS_PER_MEASURE; -static bool LoadFromBMSFile( const RString &sPath, const NameToData_t &mapNameToData, Steps &out, - const MeasureToTimeSig_t &sigAdjustments, const map &idToKeySoundIndex ) +static bool SearchForKeysound( const RString &sPath, RString nDataOriginal, map &mapFilenameToKeysoundIndex, Song &out, int &outKeysoundIndex ) { + + // Search for memoized file names: + { + RString nDataToSearchFor = nDataOriginal; + nDataToSearchFor.MakeLower(); + map::iterator it = mapFilenameToKeysoundIndex.find(nDataToSearchFor); + if (it != mapFilenameToKeysoundIndex.end()) { + outKeysoundIndex = it->second; + return true; + } + } + + // FIXME: garbled song names seem to crash the app. + // this might not be the best place to put this code. + if( !utf8_is_valid(nDataOriginal) ) + return false; + + /* Due to bugs in some programs, many BMS files have a "WAV" extension + * on files in the BMS for files that actually have some other extension. + * Do a search. Don't do a wildcard search; if sData is "song.wav", + * we might also have "song.png", which we shouldn't match. */ + RString nData = nDataOriginal; + if( !IsAFile(out.GetSongDir()+nData) ) + { + const char *exts[] = { "oga", "ogg", "wav", "mp3", NULL }; // XXX: stop duplicating these everywhere + for( unsigned i = 0; exts[i] != NULL; ++i ) + { + RString fn = SetExtension( nData, exts[i] ); + if( IsAFile(out.GetSongDir()+fn) ) + { + nData = fn; + break; + } + } + } + + if( !IsAFile(out.GetSongDir()+nData) ) + { + LOG->UserLog( "Song file", out.GetSongDir(), "references key \"%s\" that can't be found", nData.c_str() ); + return false; + } + + // Let's again search for memoized file names (we got the normalized one!): + { + RString nDataToSearchFor = nData; + nDataToSearchFor.MakeLower(); + map::iterator it = mapFilenameToKeysoundIndex.find(nDataToSearchFor); + if (it != mapFilenameToKeysoundIndex.end()) { + outKeysoundIndex = it->second; + + { + RString nDataToAdd = nDataOriginal; + nDataToAdd.MakeLower(); + mapFilenameToKeysoundIndex[nDataToAdd] = outKeysoundIndex; + } + + return true; + } + } + + // Now this is a new sample. + out.m_vsKeysoundFile.push_back( nData ); + outKeysoundIndex = out.m_vsKeysoundFile.size() - 1; + + { + RString nDataToAdd = nDataOriginal; + nDataToAdd.MakeLower(); + mapFilenameToKeysoundIndex[nDataToAdd] = outKeysoundIndex; + } + + { + RString nDataToAdd = nData; + nDataToAdd.MakeLower(); + mapFilenameToKeysoundIndex[nDataToAdd] = outKeysoundIndex; + } + + return true; + +} + +static bool SearchForKeysound( const RString &sPath, RString sNoteId, const NameToData_t &mapNameToData, map &mapIdToKeysoundIndex, map &mapFilenameToKeysoundIndex, Song &out, int &outKeysoundIndex ) +{ + + sNoteId.MakeLower(); + { + map::iterator it = mapIdToKeysoundIndex.find(sNoteId); + if (it != mapIdToKeysoundIndex.end()) + { + outKeysoundIndex = it->second; + return outKeysoundIndex >= 0; + } + } + + RString sTagToLookFor = ssprintf( "#wav%s", sNoteId.c_str() ); + RString nDataOriginal; + if( !GetTagFromMap( mapNameToData, sTagToLookFor, nDataOriginal ) ) + { + LOG->UserLog( "Song file", sPath.c_str(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); + return false; + } + + bool retval = SearchForKeysound(sPath, nDataOriginal, mapFilenameToKeysoundIndex, out, outKeysoundIndex); + mapIdToKeysoundIndex[sNoteId] = outKeysoundIndex; + return retval; + +} + +static bool LoadFromBMSFile( const RString &sPath, const NameToData_t &mapNameToData, Steps &out, Song &outSong, map &mapFilenameToKeysoundIndex ) +{ + + map mapIdToKeysoundIndex; + MeasureToTimeSig_t sigAdjustments; + LOG->Trace( "Steps::LoadFromBMSFile( '%s' )", sPath.c_str() ); out.m_StepsType = StepsType_Invalid; @@ -453,11 +565,121 @@ static bool LoadFromBMSFile( const RString &sPath, const NameToData_t &mapNameTo NoteData ndNotes; ndNotes.SetNumTracks( NUM_BMS_TRACKS ); + // Read BPM + if( GetTagFromMap(mapNameToData, "#bpm", sData) ) + { + const float fBPM = StringToFloat( sData ); + + if( fBPM > 0.0f ) + { + BPMSegment newSeg( 0, fBPM ); + out.m_Timing.AddBPMSegment( newSeg ); + LOG->Trace( "Inserting new BPM change at beat %f, BPM %f", NoteRowToBeat(0), fBPM ); + } + else + { + LOG->UserLog( "Song file", sPath.c_str(), "has an invalid BPM change at beat %f, BPM %f.", + NoteRowToBeat(0), fBPM ); + } + } + + /* Read time signatures. Note that these can differ across files in the same * song. */ MeasureToTimeSig_t mapMeasureToTimeSig; ReadTimeSigs( mapNameToData, mapMeasureToTimeSig ); + for( NameToData_t::const_iterator it = mapNameToData.lower_bound("#00000"); it != mapNameToData.end(); ++it ) + { + const RString &sName = it->first; + if( sName.size() != 6 || sName[0] != '#' || !IsAnInt( sName.substr(1,5) ) ) + continue; + // this is step or offset data. Looks like "#00705" + int iMeasureNo = atoi( sName.substr(1, 3).c_str() ); + int iBMSTrackNo = atoi( sName.substr(4, 2).c_str() ); + int iStepIndex = GetMeasureStartRow( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); + float fBeatsPerMeasure = GetBeatsPerMeasure( mapMeasureToTimeSig, iMeasureNo, sigAdjustments ); + int iRowsPerMeasure = BeatToNoteRow( fBeatsPerMeasure ); + + RString nData = it->second; + int totalPairs = nData.size() / 2; + for( int i = 0; i < totalPairs; ++i ) + { + RString sPair = nData.substr( i*2, 2 ); + + int iRow = iStepIndex + (i * iRowsPerMeasure) / totalPairs; + float fBeat = NoteRowToBeat( iRow ); + int iVal = 0; + sscanf( sPair, "%x", &iVal ); + + if (sPair == "00") + { + continue; + } + + switch( iBMSTrackNo ) + { + case BMS_TRACK_BPM: + if( iVal > 0 ) + { + out.m_Timing.SetBPMAtBeat( fBeat, (float) iVal ); + LOG->Trace( "Inserting new BPM change at beat %f, BPM %i", fBeat, iVal ); + } + else + { + LOG->UserLog( "Song file", sPath.c_str(), "has an invalid BPM change at beat %f, BPM %d.", + fBeat, iVal ); + } + break; + + case BMS_TRACK_BPM_REF: + { + RString sTagToLookFor = ssprintf( "#bpm%s", sPair.c_str() ); + RString sBPM; + if( GetTagFromMap( mapNameToData, sTagToLookFor, sBPM ) ) + { + float fBPM = StringToFloat( sBPM ); + out.m_Timing.SetBPMAtBeat( fBeat, fBPM ); + } + else + { + LOG->UserLog( "Song file", sPath.c_str(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); + } + break; + } + case BMS_TRACK_STOP: + { + if( iVal == 0 ) + { + break; + } + RString sTagToLookFor = ssprintf( "#stop%02x", iVal ); + RString sBeats; + if( GetTagFromMap( mapNameToData, sTagToLookFor, sBeats ) ) + { + // find the BPM at the time of this freeze + float fBPS = out.m_Timing.GetBPMAtBeat(fBeat) / 60.0f; + float fBeats = StringToFloat( sBeats ) / 48.0f; + float fFreezeSecs = fBeats / fBPS; + + StopSegment newSeg( BeatToNoteRow(fBeat), fFreezeSecs ); + out.m_Timing.AddStopSegment( newSeg ); + LOG->Trace( "Inserting new Freeze at beat %f, secs %f", fBeat, newSeg.m_fStopSeconds ); + } + else + { + LOG->UserLog( "Song file", sPath.c_str(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); + } + break; + } + } + } + + } + + // Now that we're done reading BPMs, factor out weird time signatures. + SetTimeSigAdjustments( mapMeasureToTimeSig, outSong, sigAdjustments ); + int iHoldStarts[NUM_BMS_TRACKS]; TapNote iHoldHeads[NUM_BMS_TRACKS]; @@ -487,10 +709,9 @@ static bool LoadFromBMSFile( const RString &sPath, const NameToData_t &mapNameTo RString sNoteId = sNoteData.substr( i, 2 ); if( sNoteId != "00" ) { - vTapNotes.push_back( TAP_ORIGINAL_TAP ); - map::const_iterator rInt = idToKeySoundIndex.find( sNoteId ); - if( rInt != idToKeySoundIndex.end() ) - vTapNotes.back().iKeysoundIndex = rInt->second; + TapNote tn = TAP_ORIGINAL_TAP; + SearchForKeysound( sPath, sNoteId, mapNameToData, mapIdToKeysoundIndex, mapFilenameToKeysoundIndex, outSong, tn.iKeysoundIndex ); + vTapNotes.push_back( tn ); } else { @@ -781,7 +1002,7 @@ static bool LoadFromBMSFile( const RString &sPath, const NameToData_t &mapNameTo return true; } -static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out, MeasureToTimeSig_t &sigAdjustmentsOut, map &idToKeySoundIndexOut ) +static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out ) { RString sData; if( GetTagFromMap(mapNameToData, "#title", sData) ) @@ -792,172 +1013,6 @@ static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out, Measur GetTagFromMap( mapNameToData, "#backbmp", out.m_sBackgroundFile ); GetTagFromMap( mapNameToData, "#wav", out.m_sMusicFile ); - if( GetTagFromMap(mapNameToData, "#bpm", sData) ) - { - const float fBPM = StringToFloat( sData ); - - if( PREFSMAN->m_bQuirksMode ) - { - BPMSegment newSeg( 0, fBPM ); - out.AddBPMSegment( newSeg ); - if( fBPM > 0.0f ) - LOG->Trace( "Inserting new positive BPM change at beat %f, BPM %f", NoteRowToBeat(0), fBPM ); - else - LOG->Trace( "Inserting new negative BPM change at beat %f, BPM %f", NoteRowToBeat(0), fBPM ); - } - else - { - if( fBPM > 0.0f ) - { - BPMSegment newSeg( 0, fBPM ); - out.AddBPMSegment( newSeg ); - LOG->Trace( "Inserting new BPM change at beat %f, BPM %f", NoteRowToBeat(0), fBPM ); - } - else - { - LOG->UserLog( "Song file", out.GetSongDir(), "has an invalid BPM change at beat %f, BPM %f.", - NoteRowToBeat(0), fBPM ); - } - } - } - - NameToData_t::const_iterator it; - for( it = mapNameToData.lower_bound("#wav"); it != mapNameToData.end(); ++it ) - { - const RString &sName = it->first; - - if( sName.size() != 6 || sName.Left(4) != "#wav" ) - continue; - - // this is keysound file name. Looks like "#WAV1A" - RString nData = it->second; - RString sWavID = sName.Right(2); - - // FIXME: garbled song names seem to crash the app. - // this might not be the best place to put this code. - if( !utf8_is_valid(nData) ) - continue; - - /* Due to bugs in some programs, many BMS files have a "WAV" extension - * on files in the BMS for files that actually have some other extension. - * Do a search. Don't do a wildcard search; if sData is "song.wav", - * we might also have "song.png", which we shouldn't match. */ - if( !IsAFile(out.GetSongDir()+nData) ) - { - const char *exts[] = { "oga", "ogg", "wav", "mp3", NULL }; // XXX: stop duplicating these everywhere - for( unsigned i = 0; exts[i] != NULL; ++i ) - { - RString fn = SetExtension( nData, exts[i] ); - if( IsAFile(out.GetSongDir()+fn) ) - { - nData = fn; - break; - } - } - } - if( !IsAFile(out.GetSongDir()+nData) ) - LOG->UserLog( "Song file", out.GetSongDir(), "references key \"%s\" that can't be found", nData.c_str() ); - - sWavID.MakeUpper(); // HACK: undo the MakeLower() - out.m_vsKeysoundFile.push_back( nData ); - idToKeySoundIndexOut[ sWavID ] = out.m_vsKeysoundFile.size()-1; - LOG->Trace( "Inserting keysound index %u '%s'", unsigned(out.m_vsKeysoundFile.size()-1), sWavID.c_str() ); - } - - // Time signature tags affect all other global timing tags, so read them first. - MeasureToTimeSig_t mapMeasureToTimeSig; - ReadTimeSigs( mapNameToData, mapMeasureToTimeSig ); - - for( it = mapNameToData.lower_bound("#00000"); it != mapNameToData.end(); ++it ) - { - const RString &sName = it->first; - if( sName.size() != 6 || sName[0] != '#' || !IsAnInt( sName.substr(1,5) ) ) - continue; - // this is step or offset data. Looks like "#00705" - int iMeasureNo = atoi( sName.substr(1, 3).c_str() ); - int iBMSTrackNo = atoi( sName.substr(4, 2).c_str() ); - int iStepIndex = GetMeasureStartRow( mapMeasureToTimeSig, iMeasureNo, sigAdjustmentsOut ); - float fBeatsPerMeasure = GetBeatsPerMeasure( mapMeasureToTimeSig, iMeasureNo, sigAdjustmentsOut ); - int iRowsPerMeasure = BeatToNoteRow( fBeatsPerMeasure ); - - RString nData = it->second; - int totalPairs = nData.size() / 2; - for( int i = 0; i < totalPairs; ++i ) - { - RString sPair = nData.substr( i*2, 2 ); - - int iRow = iStepIndex + (i * iRowsPerMeasure) / totalPairs; - float fBeat = NoteRowToBeat( iRow ); - int iVal = 0; - sscanf( sPair, "%x", &iVal ); - - if (sPair == "00") - { - continue; - } - - switch( iBMSTrackNo ) - { - case BMS_TRACK_BPM: - if( iVal > 0 ) - { - out.SetBPMAtBeat( fBeat, (float) iVal ); - LOG->Trace( "Inserting new BPM change at beat %f, BPM %i", fBeat, iVal ); - } - else - { - LOG->UserLog( "Song file", out.GetSongDir(), "has an invalid BPM change at beat %f, BPM %d.", - fBeat, iVal ); - } - break; - - case BMS_TRACK_BPM_REF: - { - RString sTagToLookFor = ssprintf( "#bpm%s", sPair.c_str() ); - RString sBPM; - if( GetTagFromMap( mapNameToData, sTagToLookFor, sBPM ) ) - { - float fBPM = StringToFloat( sBPM ); - out.SetBPMAtBeat( fBeat, fBPM ); - } - else - { - LOG->UserLog( "Song file", out.GetSongDir(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); - } - break; - } - case BMS_TRACK_STOP: - { - if( iVal == 0 ) - { - break; - } - RString sTagToLookFor = ssprintf( "#stop%02x", iVal ); - RString sBeats; - if( GetTagFromMap( mapNameToData, sTagToLookFor, sBeats ) ) - { - // find the BPM at the time of this freeze - float fBPS = out.m_Timing.GetBPMAtBeat(fBeat) / 60.0f; - float fBeats = StringToFloat( sBeats ) / 48.0f; - float fFreezeSecs = fBeats / fBPS; - - StopSegment newSeg( BeatToNoteRow(fBeat), fFreezeSecs ); - out.AddStopSegment( newSeg ); - LOG->Trace( "Inserting new Freeze at beat %f, secs %f", fBeat, newSeg.m_fStopSeconds ); - } - else - { - LOG->UserLog( "Song file", out.GetSongDir(), "has tag \"%s\" which cannot be found.", sTagToLookFor.c_str() ); - } - break; - } - } - } - - } - - // Now that we're done reading BPMs, factor out weird time signatures. - SetTimeSigAdjustments( mapMeasureToTimeSig, out, sigAdjustmentsOut ); } static void SlideDuplicateDifficulties( Song &p ) @@ -1031,7 +1086,7 @@ bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) // Create a Steps for each. vector apSteps; for( unsigned i=0; iGetDifficulty() == Difficulty_Medium ) iMainDataIndex = i; - MeasureToTimeSig_t sigAdjustments; - map idToKeysoundIndex; - ReadGlobalTags( aBMSData[iMainDataIndex], out, sigAdjustments, idToKeysoundIndex ); + ReadGlobalTags( aBMSData[iMainDataIndex], out ); // The brackets before the difficulty are in common substring, so remove them if it's found. if( commonSubstring.size() > 2 && commonSubstring[commonSubstring.size() - 2] == ' ' ) @@ -1124,10 +1177,11 @@ bool BMSLoader::LoadFromDir( const RString &sDir, Song &out ) // Now that we've parsed the keysound data, load the Steps from the rest // of the .bms files. + map mapFilenameToKeysoundIndex; for( unsigned i=0; i &Bla if( PREFSMAN->m_bQuirksMode ) { - out.AddBPMSegment( BPMSegment(0, fBPM) ); + out.m_SongTiming.AddBPMSegment( BPMSegment(0, fBPM) ); } else{ if( fBPM > 0.0f ) - out.AddBPMSegment( BPMSegment(0, fBPM) ); + out.m_SongTiming.AddBPMSegment( BPMSegment(0, fBPM) ); else LOG->UserLog( "Song file", sPath, "has an invalid BPM change at beat %f, BPM %f.", NoteRowToBeat(0), fBPM ); @@ -545,7 +545,7 @@ bool DWILoader::LoadFromDir( const RString &sPath_, Song &out, set &Bla else if( sValueName.EqualsNoCase("GAP") ) // the units of GAP is 1/1000 second - out.m_Timing.m_fBeat0OffsetInSeconds = -StringToInt( sParams[1] ) / 1000.0f; + out.m_SongTiming.m_fBeat0OffsetInSeconds = -StringToInt( sParams[1] ) / 1000.0f; else if( sValueName.EqualsNoCase("SAMPLESTART") ) out.m_fMusicSampleStartSeconds = ParseBrokenDWITimestamp(sParams[1], sParams[2], sParams[3]); @@ -570,7 +570,7 @@ bool DWILoader::LoadFromDir( const RString &sPath_, Song &out, set &Bla int iFreezeRow = BeatToNoteRow( StringToFloat(arrayFreezeValues[0]) / 4.0f ); float fFreezeSeconds = StringToFloat( arrayFreezeValues[1] ) / 1000.0f; - out.AddStopSegment( StopSegment(iFreezeRow, fFreezeSeconds) ); + out.m_SongTiming.AddStopSegment( StopSegment(iFreezeRow, fFreezeSeconds) ); // LOG->Trace( "Adding a freeze segment: beat: %f, seconds = %f", fFreezeBeat, fFreezeSeconds ); } } @@ -595,7 +595,7 @@ bool DWILoader::LoadFromDir( const RString &sPath_, Song &out, set &Bla if( fBPM > 0.0f ) { BPMSegment bs( iStartIndex, fBPM ); - out.AddBPMSegment( bs ); + out.m_SongTiming.AddBPMSegment( bs ); } else { @@ -610,7 +610,7 @@ bool DWILoader::LoadFromDir( const RString &sPath_, Song &out, set &Bla sValueName.EqualsNoCase("COUPLE") || sValueName.EqualsNoCase("SOLO") ) { - Steps* pNewNotes = new Steps; + Steps* pNewNotes = out.CreateSteps(); LoadFromDWITokens( sParams[0], sParams[1], @@ -656,7 +656,7 @@ bool DWILoader::LoadFromDir( const RString &sPath_, Song &out, set &Bla // do nothing. We don't care about this value name } } - + out.TidyUpData(); return true; } diff --git a/src/NotesLoaderJson.cpp b/src/NotesLoaderJson.cpp index 1c0501114a..e460a0eaba 100644 --- a/src/NotesLoaderJson.cpp +++ b/src/NotesLoaderJson.cpp @@ -139,7 +139,7 @@ static void Deserialize( Song &out, const Json::Value &root ) out.m_sLyricsFile = root["LyricsFile"].asString(); out.m_sCDTitleFile = root["CDTitle"].asString(); out.m_sMusicFile = root["Music"].asString(); - out.m_Timing.m_fBeat0OffsetInSeconds = (float)root["Offset"].asDouble(); + out.m_SongTiming.m_fBeat0OffsetInSeconds = (float)root["Offset"].asDouble(); out.m_fMusicSampleStartSeconds = (float)root["SampleStart"].asDouble(); out.m_fMusicSampleLengthSeconds = (float)root["SampleLength"].asDouble(); RString sSelectable = root["Selectable"].asString(); @@ -167,7 +167,7 @@ static void Deserialize( Song &out, const Json::Value &root ) out.m_fSpecifiedBPMMax = (float)root["SpecifiedBpmMax"].asDouble(); } - Deserialize( out.m_Timing, root["TimingData"] ); + Deserialize( out.m_SongTiming, root["TimingData"] ); JsonUtil::DeserializeVectorObjects( out.m_LyricSegments, Deserialize, root["LyricSegments"] ); { diff --git a/src/NotesLoaderKSF.cpp b/src/NotesLoaderKSF.cpp index a5530b3359..276f0c1e49 100644 --- a/src/NotesLoaderKSF.cpp +++ b/src/NotesLoaderKSF.cpp @@ -9,6 +9,51 @@ #include "Song.h" #include "Steps.h" +static void HandleBunki( TimingData &timing, const float fEarlyBPM, + const float fCurBPM, const float fGap, + const float fPos ) +{ + const float BeatsPerSecond = fEarlyBPM / 60.0f; + const float beat = (fPos + fGap) * BeatsPerSecond; + LOG->Trace( "BPM %f, BPS %f, BPMPos %f, beat %f", + fEarlyBPM, BeatsPerSecond, fPos, beat ); + timing.AddBPMSegment( BPMSegment(BeatToNoteRow(beat), fCurBPM) ); +} + +static bool HandlePipeChars( TimingData &timing, const RString sNoteRow, + const float fCurBeat, int &iTickCount ) +{ + RString temp = sNoteRow.substr(2,sNoteRow.size()-3); + float numTemp = StringToFloat(temp); + if (BeginsWith(sNoteRow, "|T")) + { + iTickCount = static_cast(numTemp); + timing.SetTickcountAtBeat( fCurBeat, clamp(iTickCount, 0, ROWS_PER_BEAT) ); + return true; + } + else if (BeginsWith(sNoteRow, "|B")) + { + timing.SetBPMAtBeat( fCurBeat, numTemp ); + return true; + } + else if (BeginsWith(sNoteRow, "|E")) + { + // Finally! the |E| tag is working as it should. I can die happy now -DaisuMaster + float fCurDelay = 60 / timing.GetBPMAtBeat(fCurBeat) * numTemp / iTickCount; + fCurDelay += timing.GetDelayAtRow(BeatToNoteRow(fCurBeat) ); + timing.SetStopAtBeat( fCurBeat, fCurDelay, true ); + return true; + } + else if (BeginsWith(sNoteRow, "|D")) + { + float fCurDelay = timing.GetStopAtRow(BeatToNoteRow(fCurBeat) ); + fCurDelay += numTemp / 1000; + timing.SetStopAtBeat( fCurBeat, fCurDelay, true ); + return true; + } + return false; +} + static bool LoadFromKSFFile( const RString &sPath, Steps &out, const Song &song, bool bKIUCompliant ) { LOG->Trace( "Steps::LoadFromKSFFile( '%s' )", sPath.c_str() ); @@ -25,6 +70,9 @@ static bool LoadFromKSFFile( const RString &sPath, Steps &out, const Song &song, // According to Aldo_MX, there is a default BPM and it's 60. -aj bool bDoublesChart = false; + + TimingData stepsTiming; + float SMGap1 = 0, SMGap2 = 0, BPM1 = -1, BPMPos2 = -1, BPM2 = -1, BPMPos3 = -1, BPM3 = -1; for( unsigned i=0; iUserLog( "Song file", sPath, "has an invalid tick count: %d.", iTickCount ); return false; } + stepsTiming.AddTickcountSegment(TickcountSegment(0, iTickCount)); } - else if( sValueName=="STEP" ) - { - RString theSteps = sParams[1]; - TrimLeft( theSteps ); - split( theSteps, "\n", vNoteRows, true ); - } + else if( sValueName=="DIFFICULTY" ) { out.SetMeter( max(StringToInt(sParams[1]), 0) ); @@ -60,13 +183,34 @@ static bool LoadFromKSFFile( const RString &sPath, Steps &out, const Song &song, if( sPlayer.find( "double" ) != string::npos ) bDoublesChart = true; } + // This should always be last. + else if( sValueName=="STEP" ) + { + RString theSteps = sParams[1]; + TrimLeft( theSteps ); + split( theSteps, "\n", vNoteRows, true ); + } } if( iTickCount == -1 ) { - iTickCount = 2; // Direct Move 0.5 has a default value of 4... -aj + iTickCount = 4; LOG->UserLog( "Song file", sPath, "doesn't have a TICKCOUNT. Defaulting to %i.", iTickCount ); } + + // Prepare BPM stuff already if the file uses KSF syntax. + if( bKIUCompliant ) + { + if( BPM2 > 0 && BPMPos2 > 0 ) + { + HandleBunki( stepsTiming, BPM1, BPM2, SMGap1, BPMPos2 ); + } + + if( BPM3 > 0 && BPMPos3 > 0 ) + { + HandleBunki( stepsTiming, BPM2, BPM3, SMGap2, BPMPos3 ); + } + } NoteData notedata; // read it into here @@ -133,6 +277,7 @@ static bool LoadFromKSFFile( const RString &sPath, Steps &out, const Song &song, case StepsType_pump_single: notedata.SetNumTracks( 5 ); break; case StepsType_pump_couple: notedata.SetNumTracks( 10 ); break; case StepsType_pump_double: notedata.SetNumTracks( 10 ); break; + case StepsType_pump_routine: notedata.SetNumTracks( 10 ); break; // future files may have this? case StepsType_pump_halfdouble: notedata.SetNumTracks( 6 ); break; default: FAIL_M( ssprintf("%i", out.m_StepsType) ); } @@ -146,6 +291,7 @@ static bool LoadFromKSFFile( const RString &sPath, Steps &out, const Song &song, int newTick = -1; float fCurBeat = 0.0f; float prevBeat = 0.0f; // Used for hold tails. + for( unsigned r=0; rUserLog( "Song file", sPath, "has illegal syntax \"%s\" which can't be in KIU complient files.", - sRowString.c_str() ); - return false; - //In other words: you can't mix ksf's with DM05 tags and ksf's without any DM05 tags - //Either one set or another will be read... - }*/ - if( BeginsWith(sRowString, "|B") || BeginsWith(sRowString, "|D") || BeginsWith(sRowString, "|E") ) - { - // These don't have to be worried about here: the changes and stops were already added. - continue; - } - else if ( BeginsWith(sRowString, "|T") ) - { - RString temp = sRowString.substr(2,sRowString.size()-3); - newTick = StringToInt(temp); - bTickChangeNeeded = true; + if (bKIUCompliant) + { + // Log an error, ignore the line. + continue; + } + if ( !HandlePipeChars( stepsTiming, sRowString, fCurBeat, iTickCount ) ) + { + // LOG it first. + } continue; + } + else { // Is this why improper ksf or some kiucompilant ksf mixed with dm05 ksf are ignored?? -DaisuMaster @@ -270,6 +408,7 @@ static bool LoadFromKSFFile( const RString &sPath, Steps &out, const Song &song, } out.SetNoteData( notedata ); + out.m_Timing = stepsTiming; out.TidyUpData(); @@ -358,7 +497,7 @@ static bool LoadGlobalData( const RString &sPath, Song &out, bool &bKIUCompliant else if( sValueName=="BPM" ) { BPM1 = StringToFloat(sParams[1]); - out.AddBPMSegment( BPMSegment(0, BPM1) ); + out.m_SongTiming.AddBPMSegment( BPMSegment(0, BPM1) ); } else if( sValueName=="BPM2" ) { @@ -383,7 +522,7 @@ static bool LoadGlobalData( const RString &sPath, Song &out, bool &bKIUCompliant else if( sValueName=="STARTTIME" ) { SMGap1 = -StringToFloat( sParams[1] )/100; - out.m_Timing.m_fBeat0OffsetInSeconds = SMGap1; + out.m_SongTiming.m_fBeat0OffsetInSeconds = SMGap1; } // This is currently required for more accurate KIU BPM changes. else if( sValueName=="STARTTIME2" ) @@ -409,7 +548,7 @@ static bool LoadGlobalData( const RString &sPath, Song &out, bool &bKIUCompliant TickcountSegment tcs; tcs.m_iStartRow = BeatToNoteRow(0.0f); tcs.m_iTicks = iTickCount > ROWS_PER_BEAT ? ROWS_PER_BEAT : iTickCount; - out.m_Timing.AddTickcountSegment( tcs ); + out.m_SongTiming.AddTickcountSegment( tcs ); } else if ( sValueName=="STEP" ) { @@ -465,21 +604,12 @@ static bool LoadGlobalData( const RString &sPath, Song &out, bool &bKIUCompliant { if( BPM2 > 0 && BPMPos2 > 0 ) { - const float BeatsPerSecond = BPM1 / 60.0f; - const float beat = (BPMPos2 + SMGap1) * BeatsPerSecond; - LOG->Trace( "BPM %f, BPS %f, BPMPos2 %f, beat %f", - BPM1, BeatsPerSecond, BPMPos2, beat ); - out.AddBPMSegment( BPMSegment(BeatToNoteRow(beat), BPM2) ); + HandleBunki( out.m_SongTiming, BPM1, BPM2, SMGap1, BPMPos2 ); } if( BPM3 > 0 && BPMPos3 > 0 ) { - const float BeatsPerSecond = BPM2 / 60.0f; - //The line below isn't perfect, but works better than previous versions. - const float beat = (BPMPos3 + SMGap2) * BeatsPerSecond; - LOG->Trace( "BPM %f, BPS %f, BPMPos3 %f, beat %f", - BPM2, BeatsPerSecond, BPMPos3, beat ); - out.AddBPMSegment( BPMSegment(BeatToNoteRow(beat), BPM3) ); + HandleBunki( out.m_SongTiming, BPM2, BPM3, SMGap2, BPMPos3 ); } } else @@ -507,40 +637,11 @@ static bool LoadGlobalData( const RString &sPath, Song &out, bool &bKIUCompliant BeginsWith(NoteRowString, "|D") || BeginsWith(NoteRowString, "|E") ) { bDMRequired = true; - RString temp = NoteRowString.substr(2,NoteRowString.size()-3); - float numTemp = StringToFloat(temp); - if (BeginsWith(NoteRowString, "|T")) + if ( !HandlePipeChars( out.m_SongTiming, NoteRowString, fCurBeat, iTickCount ) ) { - iTickCount = (int)numTemp; - TickcountSegment tcs; - tcs.m_iStartRow = BeatToNoteRow(fCurBeat); - tcs.m_iTicks = iTickCount > ROWS_PER_BEAT ? ROWS_PER_BEAT : iTickCount; - out.m_Timing.AddTickcountSegment( tcs ); - - continue; - } - else if (BeginsWith(NoteRowString, "|B")) - { - float fCurBpm = (float)numTemp; - //out.m_Timing.AddBPMSegment( BPMSegment( BeatToNoteRow(fCurBeat), (float)numTemp ) ); - out.m_Timing.SetBPMAtBeat( fCurBeat, fCurBpm ); - continue; - } - else if (BeginsWith(NoteRowString, "|E")) - { - // Finally! the |E| tag is working as it should. I can die happy now -DaisuMaster - float fCurDelay = 60 / out.m_Timing.GetBPMAtBeat(fCurBeat) * (float)numTemp / iTickCount; - fCurDelay += out.m_Timing.GetStopAtRow(BeatToNoteRow(fCurBeat) ); - out.m_Timing.SetStopAtBeat( fCurBeat, fCurDelay, true ); - continue; - } - else if (BeginsWith(NoteRowString, "|D")) - { - float fCurDelay = out.m_Timing.GetStopAtRow(BeatToNoteRow(fCurBeat) ); - fCurDelay += (float)numTemp / 1000; - out.m_Timing.SetStopAtBeat( fCurBeat, fCurDelay, true ); - continue; + // LOG it first. } + continue; } else { @@ -580,16 +681,19 @@ bool KSFLoader::LoadFromDir( const RString &sDir, Song &out ) ASSERT( arrayKSFFileNames.size() ); bool bKIUCompliant = false; - /* If only the first file is read, it will cause problems for other simfiles with - * different BPM changes and tickcounts. This command will probably have to be - * changed in the future. */ - if( !LoadGlobalData(out.GetSongDir() + arrayKSFFileNames[0], out, bKIUCompliant) ) + /* With Split Timing, there has to be a backup Song Timing in case + * anything goes wrong. As these files are kept in alphabetical + * order (hopefully), it is best to use the LAST file for timing + * purposes, for that is the "normal", or easiest difficulty. + * Usually. */ + unsigned files = arrayKSFFileNames.size(); + if( !LoadGlobalData(out.GetSongDir() + arrayKSFFileNames[files - 1], out, bKIUCompliant) ) return false; // load the Steps from the rest of the KSF files - for( unsigned i=0; itickSeconds * GUITAR_MIDI_COUNTS_PER_BEAT); bpmSeg.m_fBPS = float( 1. / fSecondsPerBeat ); - songOut.m_Timing.AddBPMSegment( bpmSeg ); + songOut.m_SongTiming.AddBPMSegment( bpmSeg ); } FOREACH_CONST( MidiFileIn::TimeSignatureChange, midi.timeSignatureEvents_, iter ) @@ -692,7 +692,7 @@ static bool LoadFromMidi( const RString &sPath, Song &songOut ) seg.m_iNumerator = iter->numerator; seg.m_iDenominator = iter->denominator; - songOut.m_Timing.AddTimeSignatureSegment( seg ); + songOut.m_SongTiming.AddTimeSignatureSegment( seg ); } @@ -877,13 +877,13 @@ skip_track: } } - Steps *pSteps = new Steps; + Steps *pSteps = songOut.CreateSteps(); pSteps->m_StepsType = StepsType_guitar_five; pSteps->SetDifficulty( (Difficulty)(gd+1) ); pSteps->SetNoteData( noteData ); songOut.AddSteps( pSteps ); } - + return true; } @@ -956,6 +956,7 @@ bool MidiLoader::LoadFromDir( const RString &sDir, Song &out ) if( !LoadFromMidi(sDir+vsFiles[0], out) ) return false; + out.TidyUpData(); return true; } diff --git a/src/NotesLoaderPMS.cpp b/src/NotesLoaderPMS.cpp index c5a3628e9a..f9ef5bf1cc 100644 --- a/src/NotesLoaderPMS.cpp +++ b/src/NotesLoaderPMS.cpp @@ -591,7 +591,7 @@ static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out, Measur if( fBPM > 0.0f ) { BPMSegment newSeg( 0, fBPM ); - out.AddBPMSegment( newSeg ); + out.m_SongTiming.AddBPMSegment( newSeg ); LOG->Trace( "Inserting new BPM change at beat %f, BPM %f", NoteRowToBeat(0), fBPM ); } else @@ -673,7 +673,7 @@ static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out, Measur case PMS_TRACK_BPM: if( iVal > 0 ) { - out.SetBPMAtBeat( fBeat, (float) iVal ); + out.m_SongTiming.SetBPMAtBeat( fBeat, (float) iVal ); LOG->Trace( "Inserting new BPM change at beat %f, BPM %i", fBeat, iVal ); } else @@ -694,7 +694,7 @@ static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out, Measur if( fBPM > 0.0f ) { BPMSegment newSeg( BeatToNoteRow(fBeat), fBPM ); - out.AddBPMSegment( newSeg ); + out.m_SongTiming.AddBPMSegment( newSeg ); LOG->Trace( "Inserting new BPM change at beat %f, BPM %f", fBeat, newSeg.GetBPM() ); } else @@ -716,12 +716,12 @@ static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out, Measur if( GetTagFromMap( mapNameToData, sTagToLookFor, sBeats ) ) { // find the BPM at the time of this freeze - float fBPS = out.m_Timing.GetBPMAtBeat(fBeat) / 60.0f; + float fBPS = out.m_SongTiming.GetBPMAtBeat(fBeat) / 60.0f; float fBeats = StringToFloat( sBeats ) / 48.0f; float fFreezeSecs = fBeats / fBPS; StopSegment newSeg( BeatToNoteRow(fBeat), fFreezeSecs ); - out.AddStopSegment( newSeg ); + out.m_SongTiming.AddStopSegment( newSeg ); LOG->Trace( "Inserting new Freeze at beat %f, secs %f", fBeat, newSeg.m_fStopSeconds ); } else @@ -750,7 +750,7 @@ static void ReadGlobalTags( const NameToData_t &mapNameToData, Song &out, Measur if( fBPM > 0.0f ) { BPMSegment newSeg( iStepIndex, fBPM ); - out.AddBPMSegment( newSeg ); + out.m_SongTiming.AddBPMSegment( newSeg ); LOG->Trace( "Inserting new BPM change at beat %f, BPM %f", NoteRowToBeat(newSeg.m_iStartRow), newSeg.GetBPM() ); } @@ -843,7 +843,7 @@ bool PMSLoader::LoadFromDir( const RString &sDir, Song &out ) /* Create a Steps for each. */ vector apSteps; for( unsigned i=0; i(&iLayer)) == 1 ) + enum_add(iLayer, -1); // #BGCHANGES2 = BACKGROUND_LAYER_2 + + bool bValid = iLayer>=0 && iLayerUserLog( "Song file", sPath, "has a #BGCHANGES tag \"%s\" that is out of range.", sValueName.c_str() ); + } + else + { + vector aBGChangeExpressions; + split( sParam, ",", aBGChangeExpressions ); + + for( unsigned b=0; b sBits; + split( sParams[j], "=", sBits, false ); + + // Need an identifer and a value for this to work + if( sBits.size() < 2 ) + continue; + + TrimLeft( sBits[0] ); + TrimRight( sBits[0] ); + + if( !sBits[0].CompareNoCase("TIME") ) + attack.fStartSecond = strtof( sBits[1], NULL ); + else if( !sBits[0].CompareNoCase("LEN") ) + attack.fSecsRemaining = strtof( sBits[1], NULL ); + else if( !sBits[0].CompareNoCase("END") ) + end = strtof( sBits[1], NULL ); + else if( !sBits[0].CompareNoCase("MODS") ) + { + attack.sModifiers = sBits[1]; + + if( end != -9999 ) + { + attack.fSecsRemaining = end - attack.fStartSecond; + end = -9999; + } + + if( attack.fSecsRemaining < 0.0f ) + attack.fSecsRemaining = 0.0f; + + out.m_Attacks.push_back( attack ); + } + } +} + +void SMLoader::ProcessInstrumentTracks( Song &out, const RString &sParam ) +{ + vector vs1; + split( sParam, ",", vs1 ); + FOREACH_CONST( RString, vs1, s ) + { + vector vs2; + split( *s, "=", vs2 ); + if( vs2.size() >= 2 ) + { + InstrumentTrack it = StringToInstrumentTrack( vs2[0] ); + if( it != InstrumentTrack_Invalid ) + out.m_sInstrumentTrackFile[it] = vs2[1]; + } + } +} + +bool SMLoader::ProcessBPMs( TimingData &out, const RString sParam ) +{ + vector arrayBPMChangeExpressions; + split( sParam, ",", arrayBPMChangeExpressions ); + + // prepare storage variables for negative BPMs -> Warps. + float negBeat = -1; + float negBPM = 1; + float highspeedBeat = -1; + bool bNotEmpty = false; + + for( unsigned b=0; b arrayBPMChangeValues; + split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); + // XXX: Hard to tell which file caused this. + if( arrayBPMChangeValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #BPMs value \"%s\" (must have exactly one '='), ignored.", + arrayBPMChangeExpressions[b].c_str() ); + continue; + } + + bNotEmpty = true; + + const float fBeat = StringToFloat( arrayBPMChangeValues[0] ); + const float fNewBPM = StringToFloat( arrayBPMChangeValues[1] ); + + if( fNewBPM < 0.0f ) + { + out.m_bHasNegativeBpms = true; + negBeat = fBeat; + negBPM = fNewBPM; + } + else if( fNewBPM > 0.0f ) + { + // add in a warp. + if( negBPM < 0 ) + { + float endBeat = fBeat + (fNewBPM / -negBPM) * (fBeat - negBeat); + WarpSegment new_seg(negBeat, endBeat); + out.AddWarpSegment( new_seg ); + + negBeat = -1; + negBPM = 1; + } + // too fast. make it a warp. + if( fNewBPM > FAST_BPM_WARP ) + { + highspeedBeat = fBeat; + } + else + { + // add in a warp. + if( highspeedBeat > 0 ) + { + WarpSegment new_seg(highspeedBeat, fBeat); + out.AddWarpSegment( new_seg ); + highspeedBeat = -1; + } + { + BPMSegment new_seg( BeatToNoteRow( fBeat ), fNewBPM ); + out.AddBPMSegment( new_seg ); + } + } + } + } + + return bNotEmpty; +} + +void SMLoader::ProcessStops( TimingData &out, const RString sParam ) +{ + vector arrayFreezeExpressions; + split( sParam, ",", arrayFreezeExpressions ); + + // Prepare variables for negative stop conversion. + float negBeat = -1; + float negPause = 0; + + for( unsigned f=0; f arrayFreezeValues; + split( arrayFreezeExpressions[f], "=", arrayFreezeValues ); + if( arrayFreezeValues.size() != 2 ) + { + // XXX: Hard to tell which file caused this. + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #STOPS value \"%s\" (must have exactly one '='), ignored.", + arrayFreezeExpressions[f].c_str() ); + continue; + } + + const float fFreezeBeat = StringToFloat( arrayFreezeValues[0] ); + const float fFreezeSeconds = StringToFloat( arrayFreezeValues[1] ); + + // Process the prior stop. + if( negPause > 0 ) + { + BPMSegment oldBPM = out.GetBPMSegmentAtRow(BeatToNoteRow(negBeat)); + float fSecondsPerBeat = 60 / oldBPM.GetBPM(); + float fSkipBeats = negPause / fSecondsPerBeat; + + if( negBeat + fSkipBeats > fFreezeBeat ) + fSkipBeats = fFreezeBeat - negBeat; + + WarpSegment ws( negBeat, fSkipBeats); + out.AddWarpSegment( ws ); + + negBeat = -1; + negPause = 0; + } + + if( fFreezeSeconds < 0.0f ) + { + negBeat = fFreezeBeat; + negPause = -fFreezeSeconds; + } + else if( fFreezeSeconds > 0.0f ) + { + StopSegment ss( BeatToNoteRow(fFreezeBeat), fFreezeSeconds ); + out.AddStopSegment( ss ); + } + + } + + // Process the prior stop if there was one. + if( negPause > 0 ) + { + BPMSegment oldBPM = out.GetBPMSegmentAtRow(BeatToNoteRow(negBeat)); + float fSecondsPerBeat = 60 / oldBPM.GetBPM(); + float fSkipBeats = negPause / fSecondsPerBeat; + + WarpSegment ws( negBeat, negBeat + fSkipBeats); + out.AddWarpSegment( ws ); + } +} + +void SMLoader::ProcessDelays( TimingData &out, const RString sParam ) +{ + vector arrayDelayExpressions; + split( sParam, ",", arrayDelayExpressions ); + + for( unsigned f=0; f arrayDelayValues; + split( arrayDelayExpressions[f], "=", arrayDelayValues ); + if( arrayDelayValues.size() != 2 ) + { + // XXX: Hard to tell which file caused this. + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #DELAYS value \"%s\" (must have exactly one '='), ignored.", + arrayDelayExpressions[f].c_str() ); + continue; + } + + const float fFreezeBeat = StringToFloat( arrayDelayValues[0] ); + const float fFreezeSeconds = StringToFloat( arrayDelayValues[1] ); + + StopSegment new_seg( BeatToNoteRow(fFreezeBeat), fFreezeSeconds, true ); + // XXX: Remove Negatives Bug? + new_seg.m_iStartRow = BeatToNoteRow(fFreezeBeat); + new_seg.m_fStopSeconds = fFreezeSeconds; + + // LOG->Trace( "Adding a delay segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); + + if(fFreezeSeconds > 0.0f) + out.AddStopSegment( new_seg ); + else + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid delay at beat %f, length %f.", fFreezeBeat, fFreezeSeconds ); + } +} + +void SMLoader::ProcessTimeSignatures( TimingData &out, const RString sParam ) +{ + vector vs1; + split( sParam, ",", vs1 ); + + FOREACH_CONST( RString, vs1, s1 ) + { + vector vs2; + split( *s1, "=", vs2 ); + + if( vs2.size() < 3 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with %i values.", (int)vs2.size() ); + continue; + } + + const float fBeat = StringToFloat( vs2[0] ); + + TimeSignatureSegment seg( BeatToNoteRow( fBeat ), StringToInt( vs2[1] ), StringToInt( vs2[2] )); + + if( fBeat < 0 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f.", fBeat ); + continue; + } + + if( seg.m_iNumerator < 1 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f, iNumerator %i.", fBeat, seg.m_iNumerator ); + continue; + } + + if( seg.m_iDenominator < 1 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f, iDenominator %i.", fBeat, seg.m_iDenominator ); + continue; + } + + out.AddTimeSignatureSegment( seg ); + } +} + +void SMLoader::ProcessTickcounts( TimingData &out, const RString sParam ) +{ + vector arrayTickcountExpressions; + split( sParam, ",", arrayTickcountExpressions ); + + for( unsigned f=0; f arrayTickcountValues; + split( arrayTickcountExpressions[f], "=", arrayTickcountValues ); + if( arrayTickcountValues.size() != 2 ) + { + // XXX: Hard to tell which file caused this. + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #TICKCOUNTS value \"%s\" (must have exactly one '='), ignored.", + arrayTickcountExpressions[f].c_str() ); + continue; + } + + const float fTickcountBeat = StringToFloat( arrayTickcountValues[0] ); + int iTicks = clamp(atoi( arrayTickcountValues[1] ), 0, ROWS_PER_BEAT); + + TickcountSegment new_seg( BeatToNoteRow(fTickcountBeat), iTicks ); + out.AddTickcountSegment( new_seg ); + } +} + void SMLoader::LoadTimingFromSMFile( const MsdFile &msd, TimingData &out ) { out.m_fBeat0OffsetInSeconds = 0; @@ -123,255 +443,27 @@ void SMLoader::LoadTimingFromSMFile( const MsdFile &msd, TimingData &out ) } else if( sValueName=="BPMS" ) { - vector arrayBPMChangeExpressions; - split( sParams[1], ",", arrayBPMChangeExpressions ); - - // prepare storage variables for negative BPMs -> Warps. - float negBeat = -1; - float negBPM = 1; - float highspeedBeat = -1; - - for( unsigned b=0; b arrayBPMChangeValues; - split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); - // XXX: Hard to tell which file caused this. - if( arrayBPMChangeValues.size() != 2 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayBPMChangeExpressions[b].c_str() ); - continue; - } - - const float fBeat = StringToFloat( arrayBPMChangeValues[0] ); - const float fNewBPM = StringToFloat( arrayBPMChangeValues[1] ); - - if( fNewBPM < 0.0f ) - { - out.m_bHasNegativeBpms = true; - negBeat = fBeat; - negBPM = fNewBPM; - } - else if( fNewBPM > 0.0f ) - { - // add in a warp. - if( negBPM < 0 ) - { - float endBeat = fBeat + (fNewBPM / -negBPM) * (fBeat - negBeat); - WarpSegment new_seg(negBeat, endBeat); - out.AddWarpSegment( new_seg ); - - negBeat = -1; - negBPM = 1; - } - // too fast. make it a warp. - if( fNewBPM > FAST_BPM_WARP ) - { - highspeedBeat = fBeat; - } - else - { - // add in a warp. - if( highspeedBeat > 0 ) - { - WarpSegment new_seg(highspeedBeat, fBeat); - out.AddWarpSegment( new_seg ); - highspeedBeat = -1; - } - { - BPMSegment new_seg; - new_seg.m_iStartRow = BeatToNoteRow(fBeat); - new_seg.SetBPM( fNewBPM ); - out.AddBPMSegment( new_seg ); - } - } - } - } + ProcessBPMs(out, sParams[1]); } else if( sValueName=="STOPS" || sValueName=="FREEZES" ) { - vector arrayFreezeExpressions; - split( sParams[1], ",", arrayFreezeExpressions ); - - // Prepare variables for negative stop conversion. - float negBeat = -1; - float negPause = 0; - - for( unsigned f=0; f arrayFreezeValues; - split( arrayFreezeExpressions[f], "=", arrayFreezeValues ); - if( arrayFreezeValues.size() != 2 ) - { - // XXX: Hard to tell which file caused this. - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayFreezeExpressions[f].c_str() ); - continue; - } - - const float fFreezeBeat = StringToFloat( arrayFreezeValues[0] ); - const float fFreezeSeconds = StringToFloat( arrayFreezeValues[1] ); - - // Process the prior stop. - if( negPause > 0 ) - { - BPMSegment oldBPM = out.GetBPMSegmentAtRow(BeatToNoteRow(negBeat)); - float fSecondsPerBeat = 60 / oldBPM.GetBPM(); - float fSkipBeats = negPause / fSecondsPerBeat; - - if( negBeat + fSkipBeats > fFreezeBeat ) - fSkipBeats = fFreezeBeat - negBeat; - - WarpSegment ws( negBeat, negBeat + fSkipBeats); - out.AddWarpSegment( ws ); - - negBeat = -1; - negPause = 0; - } - - if( fFreezeSeconds < 0.0f ) - { - negBeat = fFreezeBeat; - negPause = -fFreezeSeconds; - } - else if( fFreezeSeconds > 0.0f ) - { - StopSegment ss( BeatToNoteRow(fFreezeBeat), fFreezeSeconds ); - out.AddStopSegment( ss ); - } - - } - - // Process the prior stop if there was one. - if( negPause > 0 ) - { - BPMSegment oldBPM = out.GetBPMSegmentAtRow(BeatToNoteRow(negBeat)); - float fSecondsPerBeat = 60 / oldBPM.GetBPM(); - float fSkipBeats = negPause / fSecondsPerBeat; - - WarpSegment ws( negBeat, negBeat + fSkipBeats); - out.AddWarpSegment( ws ); - } + ProcessStops(out, sParams[1]); } else if( sValueName=="DELAYS" ) { - vector arrayDelayExpressions; - split( sParams[1], ",", arrayDelayExpressions ); - - for( unsigned f=0; f arrayDelayValues; - split( arrayDelayExpressions[f], "=", arrayDelayValues ); - if( arrayDelayValues.size() != 2 ) - { - // XXX: Hard to tell which file caused this. - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayDelayExpressions[f].c_str() ); - continue; - } - - const float fFreezeBeat = StringToFloat( arrayDelayValues[0] ); - const float fFreezeSeconds = StringToFloat( arrayDelayValues[1] ); - - StopSegment new_seg( BeatToNoteRow(fFreezeBeat), fFreezeSeconds, true ); - // XXX: Remove Negatives Bug? - new_seg.m_iStartRow = BeatToNoteRow(fFreezeBeat); - new_seg.m_fStopSeconds = fFreezeSeconds; - - // LOG->Trace( "Adding a delay segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); - - if(fFreezeSeconds > 0.0f) - out.AddStopSegment( new_seg ); - else - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid delay at beat %f, length %f.", fFreezeBeat, fFreezeSeconds ); - } + ProcessDelays(out, sParams[1]); } else if( sValueName=="TIMESIGNATURES" ) { - vector vs1; - split( sParams[1], ",", vs1 ); - - FOREACH_CONST( RString, vs1, s1 ) - { - vector vs2; - split( *s1, "=", vs2 ); - - if( vs2.size() < 3 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with %i values.", (int)vs2.size() ); - continue; - } - - const float fBeat = StringToFloat( vs2[0] ); - - TimeSignatureSegment seg; - seg.m_iStartRow = BeatToNoteRow(fBeat); - seg.m_iNumerator = StringToInt( vs2[1] ); - seg.m_iDenominator = StringToInt( vs2[2] ); - - if( fBeat < 0 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f.", fBeat ); - continue; - } - - if( seg.m_iNumerator < 1 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f, iNumerator %i.", fBeat, seg.m_iNumerator ); - continue; - } - - if( seg.m_iDenominator < 1 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f, iDenominator %i.", fBeat, seg.m_iDenominator ); - continue; - } - - out.AddTimeSignatureSegment( seg ); - } + ProcessTimeSignatures(out, sParams[1]); } else if( sValueName=="TICKCOUNTS" ) { - vector arrayTickcountExpressions; - split( sParams[1], ",", arrayTickcountExpressions ); - - for( unsigned f=0; f arrayTickcountValues; - split( arrayTickcountExpressions[f], "=", arrayTickcountValues ); - if( arrayTickcountValues.size() != 2 ) - { - // XXX: Hard to tell which file caused this. - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayTickcountExpressions[f].c_str() ); - continue; - } - - const float fTickcountBeat = StringToFloat( arrayTickcountValues[0] ); - int iTicks = StringToInt( arrayTickcountValues[1] ); - // you're lazy, let SM do the work for you... -DaisuMaster - if( iTicks < 1) iTicks = 1; - if( iTicks > ROWS_PER_BEAT ) iTicks = ROWS_PER_BEAT; - - TickcountSegment new_seg( BeatToNoteRow(fTickcountBeat), iTicks ); - out.AddTickcountSegment( new_seg ); - - if(iTicks >= 1 && iTicks <= ROWS_PER_BEAT ) // Constants - { - // LOG->Trace( "Adding a tickcount segment: beat: %f, ticks = %d", fTickcountBeat, iTicks ); - //out.AddTickcountSegment( new_seg ); - } - else - { - //LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid tickcount at beat %f, ticks %d.", fTickcountBeat, iTicks ); - //LOG->UserLog( "Song file", "(UNKNOWN)", "Clamping tickcount value to %d at beat %f.", iTicks, fTickcountBeat); - //etc - } - } + ProcessTickcounts(out, sParams[1]); } // Ensure all of the warps are handled right. sort(out.m_WarpSegments.begin(), out.m_WarpSegments.end()); @@ -457,8 +549,8 @@ bool SMLoader::LoadFromSMFile( const RString &sPath, Song &out, bool bFromCache return false; } - out.m_Timing.m_sFile = sPath; - LoadTimingFromSMFile( msd, out.m_Timing ); + out.m_SongTiming.m_sFile = sPath; + LoadTimingFromSMFile( msd, out.m_SongTiming ); for( unsigned i=0; i vs1; - split( sParams[1], ",", vs1 ); - FOREACH_CONST( RString, vs1, s ) - { - vector vs2; - split( *s, "=", vs2 ); - if( vs2.size() >= 2 ) - { - InstrumentTrack it = StringToInstrumentTrack( vs2[0] ); - if( it != InstrumentTrack_Invalid ) - out.m_sInstrumentTrackFile[it] = vs2[1]; - } - } + ProcessInstrumentTracks( out, sParams[1] ); } else if( sValueName=="MUSICLENGTH" ) @@ -617,27 +697,7 @@ bool SMLoader::LoadFromSMFile( const RString &sPath, Song &out, bool bFromCache else if( sValueName.Left(strlen("BGCHANGES"))=="BGCHANGES" || sValueName=="ANIMATIONS" ) { - BackgroundLayer iLayer = BACKGROUND_LAYER_1; - if( sscanf(sValueName, "BGCHANGES%d", &*ConvertValue(&iLayer)) == 1 ) - enum_add(iLayer, -1); // #BGCHANGES2 = BACKGROUND_LAYER_2 - - bool bValid = iLayer>=0 && iLayerUserLog( "Song file", sPath, "has a #BGCHANGES tag \"%s\" that is out of range.", sValueName.c_str() ); - } - else - { - vector aBGChangeExpressions; - split( sParams[1], ",", aBGChangeExpressions ); - - for( unsigned b=0; b sBits; - split( sParams[j], "=", sBits, false ); - - // Need an identifer and a value for this to work - if( sBits.size() < 2 ) - continue; - - TrimLeft( sBits[0] ); - TrimRight( sBits[0] ); - - if( !sBits[0].CompareNoCase("TIME") ) - attack.fStartSecond = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("LEN") ) - attack.fSecsRemaining = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("END") ) - end = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("MODS") ) - { - attack.sModifiers = sBits[1]; - - if( end != -9999 ) - { - attack.fSecsRemaining = end - attack.fStartSecond; - end = -9999; - } - - if( attack.fSecsRemaining < 0.0f ) - attack.fSecsRemaining = 0.0f; - - out.m_Attacks.push_back( attack ); - } - } + ProcessAttacks( out, sParams ); } else if( sValueName=="NOTES" || sValueName=="NOTES2" ) @@ -712,7 +732,7 @@ bool SMLoader::LoadFromSMFile( const RString &sPath, Song &out, bool bFromCache continue; } - Steps* pNewNotes = new Steps; + Steps* pNewNotes = out.CreateSteps(); LoadFromSMTokens( sParams[1], sParams[2], @@ -733,7 +753,7 @@ bool SMLoader::LoadFromSMFile( const RString &sPath, Song &out, bool bFromCache else LOG->UserLog( "Song file", sPath, "has an unexpected value named \"%s\".", sValueName.c_str() ); } - + TidyUpData( out, bFromCache ); return true; } @@ -846,7 +866,7 @@ bool SMLoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePath if( !bAddStepsToSong ) return true; - Steps* pNewNotes = new Steps; + Steps* pNewNotes = pSong->CreateSteps(); LoadFromSMTokens( sParams[1], sParams[2], sParams[3], sParams[4], sParams[5], sParams[6], *pNewNotes); @@ -926,6 +946,7 @@ void SMLoader::TidyUpData( Song &song, bool bFromCache ) bg.push_back( BackgroundChange(song.m_fLastBeat,song.m_sBackgroundFile) ); } while(0); } + song.TidyUpData(); } /* diff --git a/src/NotesLoaderSM.h b/src/NotesLoaderSM.h index db1e938528..d1c4feded0 100644 --- a/src/NotesLoaderSM.h +++ b/src/NotesLoaderSM.h @@ -3,11 +3,18 @@ #include "GameConstantsAndTypes.h" #include "BackgroundUtil.h" +#include "MsdFile.h" // we require the struct from here. -class MsdFile; class Song; class Steps; class TimingData; + +/** + * @brief The highest allowable speed before Warps come in. + * + * This was brought in from StepMania 4's recent betas. */ +const float FAST_BPM_WARP = 9999999.f; + /** @brief Reads a Song from an .SM file. */ namespace SMLoader { @@ -25,6 +32,17 @@ namespace SMLoader bool LoadEditFromBuffer( const RString &sBuffer, const RString &sEditFilePath, ProfileSlot slot ); bool LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePath, ProfileSlot slot, bool bAddStepsToSong ); bool LoadFromBGChangesString( BackgroundChange &change, const RString &sBGChangeExpression ); + + + bool ProcessBPMs( TimingData &, const RString ); + void ProcessStops( TimingData &, const RString ); + void ProcessDelays( TimingData &, const RString ); + void ProcessTimeSignatures( TimingData &, const RString ); + void ProcessTickcounts( TimingData &, const RString ); + void ProcessBGChanges( Song &out, const RString &sValueName, + const RString &sPath, const RString &sParam ); + void ProcessAttacks( Song &out, MsdFile::value_t sParams ); + void ProcessInstrumentTracks( Song &out, const RString &sParam ); } #endif diff --git a/src/NotesLoaderSMA.cpp b/src/NotesLoaderSMA.cpp index 089e0705bb..8caba0a62b 100644 --- a/src/NotesLoaderSMA.cpp +++ b/src/NotesLoaderSMA.cpp @@ -12,12 +12,379 @@ #include "Song.h" #include "SongManager.h" #include "Steps.h" +#include "Attack.h" /** * @brief A custom .edit file can only be so big before we have to reject it. */ const int MAX_EDIT_STEPS_SIZE_BYTES = 60*1024; // 60 KB +bool SMALoader::LoadFromBGChangesString( BackgroundChange &change, + const RString &sBGChangeExpression ) +{ + return SMLoader::LoadFromBGChangesString(change, sBGChangeExpression); +} + +bool SMALoader::LoadFromDir( const RString &sPath, Song &out ) +{ + vector aFileNames; + GetApplicableFiles( sPath, aFileNames ); + + if( aFileNames.size() > 1 ) + { + LOG->UserLog( "Song", sPath, "has more than one SMA file. Only one SMA file is allowed per song." ); + return false; + } + ASSERT( aFileNames.size() == 1 ); + return LoadFromSMAFile( sPath + aFileNames[0], out ); +} + +float SMALoader::RowToBeat( RString sLine, const int iRowsPerBeat ) +{ + if( sLine.find("R") || sLine.find("r") ) + { + sLine = sLine.Left(sLine.size()-1); + return StringToFloat( sLine ) / iRowsPerBeat; + } + else + { + return StringToFloat( sLine ); + } +} + +bool SMALoader::ProcessBPMs( TimingData &out, const int iRowsPerBeat, const RString sParam ) +{ + vector arrayBPMChangeExpressions; + split( sParam, ",", arrayBPMChangeExpressions ); + + // prepare storage variables for negative BPMs -> Warps. + float negBeat = -1; + float negBPM = 1; + float highspeedBeat = -1; + bool bNotEmpty = false; + + for( unsigned b=0; b arrayBPMChangeValues; + split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); + // XXX: Hard to tell which file caused this. + if( arrayBPMChangeValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #BPMs value \"%s\" (must have exactly one '='), ignored.", + arrayBPMChangeExpressions[b].c_str() ); + continue; + } + + bNotEmpty = true; + + const float fBeat = RowToBeat( arrayBPMChangeValues[0], iRowsPerBeat ); + const float fNewBPM = StringToFloat( arrayBPMChangeValues[1] ); + + if( fNewBPM < 0.0f ) + { + out.m_bHasNegativeBpms = true; + negBeat = fBeat; + negBPM = fNewBPM; + } + else if( fNewBPM > 0.0f ) + { + // add in a warp. + if( negBPM < 0 ) + { + float endBeat = fBeat + (fNewBPM / -negBPM) * (fBeat - negBeat); + WarpSegment new_seg(negBeat, endBeat); + out.AddWarpSegment( new_seg ); + + negBeat = -1; + negBPM = 1; + } + // too fast. make it a warp. + if( fNewBPM > FAST_BPM_WARP ) + { + highspeedBeat = fBeat; + } + else + { + // add in a warp. + if( highspeedBeat > 0 ) + { + WarpSegment new_seg(highspeedBeat, fBeat); + out.AddWarpSegment( new_seg ); + highspeedBeat = -1; + } + { + BPMSegment new_seg( BeatToNoteRow( fBeat ), fNewBPM ); + out.AddBPMSegment( new_seg ); + } + } + } + } + + return bNotEmpty; +} + +void SMALoader::ProcessStops( TimingData &out, const int iRowsPerBeat, const RString sParam ) +{ + vector arrayFreezeExpressions; + split( sParam, ",", arrayFreezeExpressions ); + + // Prepare variables for negative stop conversion. + float negBeat = -1; + float negPause = 0; + + for( unsigned f=0; f arrayFreezeValues; + split( arrayFreezeExpressions[f], "=", arrayFreezeValues ); + if( arrayFreezeValues.size() != 2 ) + { + // XXX: Hard to tell which file caused this. + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #STOPS value \"%s\" (must have exactly one '='), ignored.", + arrayFreezeExpressions[f].c_str() ); + continue; + } + + const float fFreezeBeat = RowToBeat( arrayFreezeValues[0], iRowsPerBeat ); + const float fFreezeSeconds = StringToFloat( arrayFreezeValues[1] ); + + // Process the prior stop. + if( negPause > 0 ) + { + BPMSegment oldBPM = out.GetBPMSegmentAtRow(BeatToNoteRow(negBeat)); + float fSecondsPerBeat = 60 / oldBPM.GetBPM(); + float fSkipBeats = negPause / fSecondsPerBeat; + + if( negBeat + fSkipBeats > fFreezeBeat ) + fSkipBeats = fFreezeBeat - negBeat; + + WarpSegment ws( negBeat, negBeat + fSkipBeats); + out.AddWarpSegment( ws ); + + negBeat = -1; + negPause = 0; + } + + if( fFreezeSeconds < 0.0f ) + { + negBeat = fFreezeBeat; + negPause = -fFreezeSeconds; + } + else if( fFreezeSeconds > 0.0f ) + { + StopSegment ss( BeatToNoteRow(fFreezeBeat), fFreezeSeconds ); + out.AddStopSegment( ss ); + } + + } + + // Process the prior stop if there was one. + if( negPause > 0 ) + { + BPMSegment oldBPM = out.GetBPMSegmentAtRow(BeatToNoteRow(negBeat)); + float fSecondsPerBeat = 60 / oldBPM.GetBPM(); + float fSkipBeats = negPause / fSecondsPerBeat; + + WarpSegment ws( negBeat, negBeat + fSkipBeats); + out.AddWarpSegment( ws ); + } +} + +void SMALoader::ProcessDelays( TimingData &out, const int iRowsPerBeat, const RString sParam ) +{ + vector arrayDelayExpressions; + split( sParam, ",", arrayDelayExpressions ); + + for( unsigned f=0; f arrayDelayValues; + split( arrayDelayExpressions[f], "=", arrayDelayValues ); + if( arrayDelayValues.size() != 2 ) + { + // XXX: Hard to tell which file caused this. + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #DELAYS value \"%s\" (must have exactly one '='), ignored.", + arrayDelayExpressions[f].c_str() ); + continue; + } + + const float fFreezeBeat = RowToBeat( arrayDelayValues[0], iRowsPerBeat ); + const float fFreezeSeconds = StringToFloat( arrayDelayValues[1] ); + + StopSegment new_seg( BeatToNoteRow(fFreezeBeat), fFreezeSeconds, true ); + // XXX: Remove Negatives Bug? + new_seg.m_iStartRow = BeatToNoteRow(fFreezeBeat); + new_seg.m_fStopSeconds = fFreezeSeconds; + + // LOG->Trace( "Adding a delay segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); + + if(fFreezeSeconds > 0.0f) + out.AddStopSegment( new_seg ); + else + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid delay at beat %f, length %f.", fFreezeBeat, fFreezeSeconds ); + } +} + +void SMALoader::ProcessTickcounts( TimingData &out, const int iRowsPerBeat, const RString sParam ) +{ + vector arrayTickcountExpressions; + split( sParam, ",", arrayTickcountExpressions ); + + for( unsigned f=0; f arrayTickcountValues; + split( arrayTickcountExpressions[f], "=", arrayTickcountValues ); + if( arrayTickcountValues.size() != 2 ) + { + // XXX: Hard to tell which file caused this. + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #TICKCOUNTS value \"%s\" (must have exactly one '='), ignored.", + arrayTickcountExpressions[f].c_str() ); + continue; + } + + const float fTickcountBeat = RowToBeat( arrayTickcountValues[0], iRowsPerBeat ); + int iTicks = clamp(atoi( arrayTickcountValues[1] ), 0, ROWS_PER_BEAT); + + TickcountSegment new_seg( BeatToNoteRow(fTickcountBeat), iTicks ); + out.AddTickcountSegment( new_seg ); + } +} + +void SMALoader::ProcessMultipliers( TimingData &out, const int iRowsPerBeat, const RString sParam ) +{ + vector arrayMultiplierExpressions; + split( sParam, ",", arrayMultiplierExpressions ); + + for( unsigned f=0; f arrayMultiplierValues; + split( arrayMultiplierExpressions[f], "=", arrayMultiplierValues ); + if( arrayMultiplierValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #MULTIPLIER value \"%s\" (must have exactly one '='), ignored.", + arrayMultiplierExpressions[f].c_str() ); + continue; + } + const float fComboBeat = RowToBeat( arrayMultiplierValues[0], iRowsPerBeat ); + const int iCombos = StringToInt( arrayMultiplierValues[1] ); + ComboSegment new_seg( BeatToNoteRow( fComboBeat ), iCombos ); + out.AddComboSegment( new_seg ); + } +} + +void SMALoader::ProcessBeatsPerMeasure( TimingData &out, const RString sParam ) +{ + vector vs1; + split( sParam, ",", vs1 ); + + FOREACH_CONST( RString, vs1, s1 ) + { + vector vs2; + split( *s1, "=", vs2 ); + + if( vs2.size() < 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid beats per measure change with %i values.", (int)vs2.size() ); + continue; + } + + const float fBeat = StringToFloat( vs2[0] ); + + TimeSignatureSegment seg( BeatToNoteRow( fBeat ), StringToInt( vs2[1] ), 4 ); + + if( fBeat < 0 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f.", fBeat ); + continue; + } + + if( seg.m_iNumerator < 1 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f, iNumerator %i.", fBeat, seg.m_iNumerator ); + continue; + } + + out.AddTimeSignatureSegment( seg ); + } +} + +float BeatToSeconds(float fromBeat, RString toSomething) +{ + return 0; +} + +void SMALoader::ProcessSpeeds( TimingData &out, const int iRowsPerBeat, const RString sParam ) +{ + vector vs1; + split( sParam, ",", vs1 ); + + FOREACH_CONST( RString, vs1, s1 ) + { + vector vs2; + split( *s1, "=", vs2 ); + + if( RowToBeat(vs2[0], iRowsPerBeat) == 0 && vs2.size() == 2 ) // First one always seems to have 2. + { + vs2.push_back("0"); + } + + if( vs2.size() < 3 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an speed change with %i values.", (int)vs2.size() ); + continue; + } + + const float fBeat = RowToBeat( vs2[0], iRowsPerBeat ); + + unsigned short tmp = ( (vs2[2].find("s") || vs2[2].find("S") ) + ? 1 : 0); + + SpeedSegment seg( fBeat, StringToFloat( vs2[1] ), StringToFloat( vs2[2] ), tmp); + + if( fBeat < 0 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an speed change with beat %f.", fBeat ); + continue; + } + + if( seg.m_fWait < 0 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an speed change with beat %f, fWait %f.", fBeat, seg.m_fWait ); + continue; + } + + out.AddSpeedSegment( seg ); + } +} + +void SMALoader::ProcessFakes( TimingData &out, const int iRowsPerBeat, const RString sParam ) +{ + vector arrayFakeExpressions; + split( sParam, ",", arrayFakeExpressions ); + + for( unsigned b=0; b arrayFakeValues; + split( arrayFakeExpressions[b], "=", arrayFakeValues ); + // XXX: Hard to tell which file caused this. + if( arrayFakeValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #FAKES value \"%s\" (must have exactly one '='), ignored.", + arrayFakeExpressions[b].c_str() ); + continue; + } + + const float fBeat = RowToBeat( arrayFakeValues[0], iRowsPerBeat ); + const float fNewBeat = StringToFloat( arrayFakeValues[1] ); + + if(fNewBeat > 0) + out.AddFakeSegment( FakeSegment(fBeat, fNewBeat) ); + else + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid Fake at beat %f, BPM %f.", fBeat, fNewBeat ); + } + } +} + + void SMALoader::LoadFromSMATokens( RString sStepsType, RString sDescription, @@ -28,73 +395,9 @@ void SMALoader::LoadFromSMATokens( Steps &out ) { - // we're loading from disk, so this is by definition already saved: - out.SetSavedToDisk( true ); - - Trim( sStepsType ); - Trim( sDescription ); - Trim( sDifficulty ); - Trim( sNoteData ); - - // LOG->Trace( "Steps::LoadFromSMTokens()" ); - - // insert stepstype hacks from GameManager.cpp here? -aj - out.m_StepsType = GAMEMAN->StringToStepsType( sStepsType ); - out.SetDescription( sDescription ); - out.SetCredit( sDescription ); // this is often used for both. - out.SetDifficulty( StringToDifficulty(sDifficulty) ); - - sDescription.MakeLower(); - - // Handle hacks that originated back when StepMania didn't have - // Difficulty_Challenge. (At least v1.64, possibly v3.0 final...) - if( out.GetDifficulty() == Difficulty_Hard ) - { - // HACK: SMANIAC used to be Difficulty_Hard with a special description. - if( sDescription == "smaniac" ) - out.SetDifficulty( Difficulty_Challenge ); - - // HACK: CHALLENGE used to be Difficulty_Hard with a special description. - if( sDescription == "challenge" ) - out.SetDifficulty( Difficulty_Challenge ); - } - - out.SetMeter( StringToInt(sMeter) ); - vector saValues; - split( sRadarValues, ",", saValues, true ); - int categories = NUM_RadarCategory - 1; // Fakes aren't counted in the radar values. - if( saValues.size() == (unsigned)categories * NUM_PLAYERS ) - { - RadarValues v[NUM_PLAYERS]; - FOREACH_PlayerNumber( pn ) - { - // Can't use the foreach anymore due to flexible radar lines. - for( RadarCategory rc = (RadarCategory)0; rc < categories; - enum_add( rc, 1 ) ) - { - v[pn][rc] = StringToFloat( saValues[pn*categories + rc] ); - } - } - out.SetCachedRadarValues( v ); - } - - out.SetSMNoteData( sNoteData ); - - out.TidyUpData(); -} - -bool SMALoader::LoadFromDir( const RString &sPath, Song &out ) -{ - vector aFileNames; - GetApplicableFiles( sPath, aFileNames ); - - if( aFileNames.size() > 1 ) - { - LOG->UserLog( "Song", sPath, "has more than one SMA file. There can be only one!" ); - return false; - } - ASSERT( aFileNames.size() == 1 ); - return LoadFromSMAFile( sPath + aFileNames[0], out ); + SMLoader::LoadFromSMTokens( sStepsType, sDescription, + sDifficulty, sMeter, sRadarValues, + sNoteData, out ); } void SMALoader::TidyUpData( Song &song, bool bFromCache ) @@ -113,8 +416,12 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) return false; } - out.m_Timing.m_sFile = sPath; - LoadTimingFromSMAFile( msd, out.m_Timing ); + out.m_SongTiming.m_sFile = sPath; // songs still have their fallback timing. + + int state = SMA_GETTING_SONG_INFO; + Steps* pNewNotes = NULL; + TimingData stepsTiming; + int iRowsPerBeat = -1; // Start with an invalid value: needed for checking. for( unsigned i=0; i vs1; - split( sParams[1], ",", vs1 ); - FOREACH_CONST( RString, vs1, s ) - { - vector vs2; - split( *s, "=", vs2 ); - if( vs2.size() >= 2 ) - { - InstrumentTrack it = StringToInstrumentTrack( vs2[0] ); - if( it != InstrumentTrack_Invalid ) - out.m_sInstrumentTrackFile[it] = vs2[1]; - } - } + SMLoader::ProcessInstrumentTracks( out, sParams[1] ); } else if( sValueName=="MUSICLENGTH" ) @@ -202,7 +497,8 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) } else if( sValueName=="LASTBEAT" ) { - ; } + ; + } else if( sValueName=="SONGFILENAME" ) { ; @@ -226,6 +522,11 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) //else if( sValueName=="SAMPLEPATH" ) //out.m_sMusicSamplePath = sParams[1]; + else if( sValueName=="LISTSORT" ) + { + ; + } + else if( sValueName=="DISPLAYBPM" ) { // #DISPLAYBPM:[xxx][xxx:xxx]|[*]; @@ -242,6 +543,41 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) } } + else if( sValueName=="SMAVERSION" ) + { + ; // ignore it. + } + + else if( sValueName=="ROWSPERBEAT" ) + { + /* This value is used to help translate the timings + * the SMA format uses. Starting with the second + * appearance, it delimits NoteData. Right now, this + * value doesn't seem to be editable in SMA. When it + * becomes so, make adjustments to this code. */ + if( iRowsPerBeat < 0 ) + { + vector arrayBeatChangeExpressions; + split( sParams[1], ",", arrayBeatChangeExpressions ); + + vector arrayBeatChangeValues; + split( arrayBeatChangeExpressions[0], "=", arrayBeatChangeValues ); + iRowsPerBeat = StringToInt(arrayBeatChangeValues[1]); + } + else + { + state = SMA_GETTING_STEP_INFO; + pNewNotes = new Steps; + } + } + + else if( sValueName=="BEATSPERMEASURE" ) + { + TimingData &timing = (state == SMA_GETTING_STEP_INFO + ? pNewNotes->m_Timing : out.m_SongTiming); + ProcessBeatsPerMeasure( timing, sParams[1] ); + } + else if( sValueName=="SELECTABLE" ) { if(sParams[1].EqualsNoCase("YES")) @@ -265,27 +601,7 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) else if( sValueName.Left(strlen("BGCHANGES"))=="BGCHANGES" || sValueName=="ANIMATIONS" ) { - BackgroundLayer iLayer = BACKGROUND_LAYER_1; - if( sscanf(sValueName, "BGCHANGES%d", &*ConvertValue(&iLayer)) == 1 ) - enum_add(iLayer, -1); // #BGCHANGES2 = BACKGROUND_LAYER_2 - - bool bValid = iLayer>=0 && iLayerUserLog( "Song file", sPath, "has a #BGCHANGES tag \"%s\" that is out of range.", sValueName.c_str() ); - } - else - { - vector aBGChangeExpressions; - split( sParams[1], ",", aBGChangeExpressions ); - - for( unsigned b=0; bm_Timing : out.m_SongTiming); + timing.m_fBeat0OffsetInSeconds = StringToFloat( sParams[1] ); + } + + else if( sValueName=="BPMS" ) + { + TimingData &timing = (state == SMA_GETTING_STEP_INFO + ? pNewNotes->m_Timing : out.m_SongTiming); + ProcessBPMs( timing, iRowsPerBeat, sParams[1] ); + } + + else if( sValueName=="STOPS" || sValueName=="FREEZES" ) + { + TimingData &timing = (state == SMA_GETTING_STEP_INFO + ? pNewNotes->m_Timing : out.m_SongTiming); + ProcessStops( timing, iRowsPerBeat, sParams[1] ); + } + + else if( sValueName=="DELAYS" ) + { + TimingData &timing = (state == SMA_GETTING_STEP_INFO + ? pNewNotes->m_Timing : out.m_SongTiming); + ProcessDelays( timing, iRowsPerBeat, sParams[1] ); + } + + else if( sValueName=="TICKCOUNT" ) + { + TimingData &timing = (state == SMA_GETTING_STEP_INFO + ? pNewNotes->m_Timing : out.m_SongTiming); + ProcessTickcounts( timing, iRowsPerBeat, sParams[1] ); + } + + else if( sValueName=="SPEED" ) + { + TimingData &timing = (state == SMA_GETTING_STEP_INFO + ? pNewNotes->m_Timing : out.m_SongTiming); + ProcessSpeeds( timing, iRowsPerBeat, sParams[1] ); + } + + else if( sValueName=="MULTIPLIER" ) + { + ProcessMultipliers( pNewNotes->m_Timing, iRowsPerBeat, sParams[1] ); + } + + else if( sValueName=="FAKES" ) + { + TimingData &timing = (state == SMA_GETTING_STEP_INFO + ? pNewNotes->m_Timing : out.m_SongTiming); + ProcessFakes( timing, iRowsPerBeat, sParams[1] ); + } + + else if( sValueName=="METERTYPE" ) + { + ; // We don't use this...yet. + } + else if( sValueName=="KEYSOUNDS" ) { split( sParams[1], ",", out.m_vsKeysoundFile ); @@ -309,47 +684,7 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) // Attacks loaded from file else if( sValueName=="ATTACKS" ) { - // Build the RString vector here so we can write it to file again later - for( unsigned s=1; s < sParams.params.size(); ++s ) - out.m_sAttackString.push_back( sParams[s] ); - - Attack attack; - float end = -9999; - - for( unsigned j=1; j < sParams.params.size(); ++j ) - { - vector sBits; - split( sParams[j], "=", sBits, false ); - - // Need an identifer and a value for this to work - if( sBits.size() < 2 ) - continue; - - TrimLeft( sBits[0] ); - TrimRight( sBits[0] ); - - if( !sBits[0].CompareNoCase("TIME") ) - attack.fStartSecond = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("LEN") ) - attack.fSecsRemaining = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("END") ) - end = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("MODS") ) - { - attack.sModifiers = sBits[1]; - - if( end != -9999 ) - { - attack.fSecsRemaining = end - attack.fStartSecond; - end = -9999; - } - - if( attack.fSecsRemaining < 0.0f ) - attack.fSecsRemaining = 0.0f; - - out.m_Attacks.push_back( attack ); - } - } + SMLoader::ProcessAttacks( out, sParams ); } else if( sValueName=="NOTES" || sValueName=="NOTES2" ) @@ -360,7 +695,6 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) continue; } - Steps* pNewNotes = new Steps; LoadFromSMATokens( sParams[1], sParams[2], @@ -372,16 +706,13 @@ bool SMALoader::LoadFromSMAFile( const RString &sPath, Song &out ) out.AddSteps( pNewNotes ); } - /* - * We used to check for timing data in this section. That has - * since been moved to a dedicated function. - */ - else if( sValueName=="OFFSET" || sValueName=="BPMS" || sValueName=="STOPS" || sValueName=="FREEZES" || sValueName=="DELAYS" || sValueName=="TIMESIGNATURES" || sValueName=="LEADTRACK" || sValueName=="TICKCOUNTS" ) + else if( sValueName=="TIMESIGNATURES" || sValueName=="LEADTRACK" ) ; else LOG->UserLog( "Song file", sPath, "has an unexpected value named \"%s\".", sValueName.c_str() ); } - + TidyUpData(out, false); + out.TidyUpData(); return true; } @@ -390,236 +721,6 @@ void SMALoader::GetApplicableFiles( const RString &sPath, vector &out ) GetDirListing( sPath + RString("*.sma"), out ); } -bool SMALoader::LoadTimingFromFile( const RString &fn, TimingData &out ) -{ - MsdFile msd; - if( !msd.ReadFile( fn, true ) ) // unescape - { - LOG->UserLog( "Song file", fn, "couldn't be loaded: %s", msd.GetError().c_str() ); - return false; - } - - out.m_sFile = fn; - LoadTimingFromSMAFile( msd, out ); - return true; -} - -void SMALoader::LoadTimingFromSMAFile( const MsdFile &msd, TimingData &out ) -{ - out.m_fBeat0OffsetInSeconds = 0; - out.m_BPMSegments.clear(); - out.m_StopSegments.clear(); - out.m_WarpSegments.clear(); - out.m_vTimeSignatureSegments.clear(); - - vector arrayWarpsFromNegativeBPMs; - //vector arrayWarpsFromNegativeStops; - int rowsPerMeasure = 0; - bool encountered = false; - - for( unsigned i=0; i arrayFreezeExpressions; - split( sParams[1], ",", arrayFreezeExpressions ); - - for( unsigned f=0; f arrayFreezeValues; - split( arrayFreezeExpressions[f], "=", arrayFreezeValues ); - if( arrayFreezeValues.size() != 2 ) - { - // XXX: Hard to tell which file caused this. - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayFreezeExpressions[f].c_str() ); - continue; - } - - float fFreezeBeat = 0; - RString beat = arrayFreezeValues[0]; - if( beat.Right(0).MakeUpper() == "R" ) - { - beat = beat.Left(beat.size()-1); - fFreezeBeat = StringToFloat( beat ) / rowsPerMeasure; - } - else - { - fFreezeBeat = StringToFloat(beat); - } - - //float fFreezeBeat = StringToFloat( arrayBPMChangeValues[0] ); - const float fFreezeSeconds = StringToFloat( arrayFreezeValues[1] ); - StopSegment new_seg( BeatToNoteRow(fFreezeBeat), fFreezeSeconds ); - // XXX: Remove Negatives Bug? - new_seg.m_iStartRow = BeatToNoteRow(fFreezeBeat); - new_seg.m_fStopSeconds = fFreezeSeconds; - - if(fFreezeSeconds > 0.0f) - { - // LOG->Trace( "Adding a freeze segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); - out.AddStopSegment( new_seg ); - } - else - { - // negative stops (hi JS!) -aj - if( PREFSMAN->m_bQuirksMode ) - { - // LOG->Trace( "Adding a negative freeze segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); - out.AddStopSegment( new_seg ); - } - else - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid stop at beat %f, length %f.", fFreezeBeat, fFreezeSeconds ); - } - } - } - - else if( sValueName=="BPMS" ) - { - vector arrayBPMChangeExpressions; - split( sParams[1], ",", arrayBPMChangeExpressions ); - - for( unsigned b=0; b arrayBPMChangeValues; - split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); - // XXX: Hard to tell which file caused this. - if( arrayBPMChangeValues.size() != 2 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayBPMChangeExpressions[b].c_str() ); - continue; - } - - float fBeat = 0; - RString beat = arrayBPMChangeValues[0]; - if( beat.Right(0).MakeUpper() == "R" ) - { - beat = beat.Left(beat.size()-1); - fBeat = StringToFloat( beat ) / rowsPerMeasure; - } - else - { - fBeat = StringToFloat(beat); - } - - //float fBeat = StringToFloat( arrayBPMChangeValues[0] ); - const float fNewBPM = StringToFloat( arrayBPMChangeValues[1] ); - // XXX: Remove Negatives Bug? - BPMSegment new_seg; - new_seg.m_iStartRow = BeatToNoteRow(fBeat); - new_seg.SetBPM( fNewBPM ); - - // convert negative BPMs into Warp segments - if( fNewBPM < 0.0f ) - { - vector arrayNextBPMChangeValues; - // get next bpm in sequence - if((b+1) < arrayBPMChangeExpressions.size()) - { - split( arrayBPMChangeExpressions[b+1], "=", arrayNextBPMChangeValues ); - const float fNextPositiveBeat = StringToFloat( arrayNextBPMChangeValues[0] ); - const float fNextPositiveBPM = StringToFloat( arrayNextBPMChangeValues[1] ); - - // tJumpPos = (tPosBPS-abs(negBPS)) + (gPosBPMPosition - fNegPosition) - float fDeltaBeat = ((fNextPositiveBPM/60.0f)-abs(fNewBPM/60.0f)) + (fNextPositiveBeat-fBeat); - //float fWarpLengthBeats = fNextPositiveBeat + fDeltaBeat; - WarpSegment wsTemp(BeatToNoteRow(fBeat),fDeltaBeat); - arrayWarpsFromNegativeBPMs.push_back(wsTemp); - - /* - LOG->Trace( ssprintf("==NotesLoSM negbpm==\nfnextposbeat = %f, fnextposbpm = %f,\nfdelta = %f, fwarpto = %f", - fNextPositiveBeat, - fNextPositiveBPM, - fDeltaBeat, - fWarpToBeat - ) ); - */ - /* - LOG->Trace( ssprintf("==Negative/Subtractive BPM in NotesLoader==\nNegBPM has noterow = %i, BPM = %f\nNextBPM @ noterow %i\nDelta value = %i noterows\nThis warp will have us end up at noterow %i", - BeatToNoteRow(fBeat), fNewBPM, - BeatToNoteRow(fNextPositiveBeat), - BeatToNoteRow(fDeltaBeat), - BeatToNoteRow(fWarpToBeat)) - ); - */ - //float fDeltaBeat = ((fNextPositiveBPM/60.0f)-abs(fNewBPM/60.0f)) + (fNextPositiveBeat-fBeat); - /* - LOG->Trace( ssprintf("==NotesLoader Delta as NoteRows==\nfDeltaBeat = %f (beat)\nfDeltaBeat = (NextBPMSeg %f - abs(fBPS %f)) + (nextStartRow %i - thisRow %i)", - fDeltaBeat,(fNextPositiveBPM/60.0f),abs(fNewBPM/60.0f),BeatToNoteRow(fNextPositiveBeat),BeatToNoteRow(fBeat)) - ); - */ - - out.AddBPMSegment( new_seg ); - - continue; - } - else - { - // last BPM is a negative one? ugh. -aj (MAX_NOTE_ROW exists btw) - out.AddBPMSegment( new_seg ); - } - } - - if(fNewBPM > 0.0f) - out.AddBPMSegment( new_seg ); - else - { - out.m_bHasNegativeBpms = true; - // only add Negative BPMs in quirks mode -aj - if( PREFSMAN->m_bQuirksMode ) - out.AddBPMSegment( new_seg ); - else - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid BPM change at beat %f, BPM %f.", fBeat, fNewBPM ); - } - } - } - - - // Note: Even though it is possible to have Negative BPMs and Stops in - // a song along with Warps, we should not support files that contain - // both styles of warp tricks (Negatives vs. #WARPS). - // If Warps have been populated from Negative BPMs, then go through that - // instead of using the data in the Warps tag. This should be above, - // but it breaks compiling so... - if(arrayWarpsFromNegativeBPMs.size() > 0) - { - // zomg we already have some warps... - for( unsigned j=0; jTrace( "SMALoader::LoadEditFromFile(%s)", sEditFilePath.c_str() ); @@ -702,7 +803,7 @@ bool SMALoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePat if( !bAddStepsToSong ) return true; - Steps* pNewNotes = new Steps; + Steps* pNewNotes = pSong->CreateSteps(); LoadFromSMATokens( sParams[1], sParams[2], sParams[3], sParams[4], sParams[5], sParams[6], *pNewNotes); @@ -730,12 +831,6 @@ bool SMALoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePat return true; } -bool SMALoader::LoadFromBGChangesString( BackgroundChange &change, - const RString &sBGChangeExpression ) -{ - return SMLoader::LoadFromBGChangesString(change, sBGChangeExpression); -} - /** * @file * @author Aldo Fregoso, Jason Felds (c) 2009-2011 diff --git a/src/NotesLoaderSMA.h b/src/NotesLoaderSMA.h index 0fceb3977d..c9e3dfa811 100644 --- a/src/NotesLoaderSMA.h +++ b/src/NotesLoaderSMA.h @@ -9,6 +9,16 @@ class Song; class Steps; class TimingData; +/** + * @brief The various states while parsing a .sma file. + */ +enum SMALoadingStates +{ + SMA_GETTING_SONG_INFO, /**< Retrieving song information. */ + SMA_GETTING_STEP_INFO, /**< Retrieving step information. */ + NUM_SMALoadingStates /**< The number of states used. */ +}; + /** @brief Reads a Song from a .SMA file. */ namespace SMALoader { @@ -25,12 +35,22 @@ namespace SMALoader bool LoadFromSMAFile( const RString &sPath, Song &out ); void GetApplicableFiles( const RString &sPath, vector &out ); - bool LoadTimingFromFile( const RString &fn, TimingData &out ); - void LoadTimingFromSMAFile( const MsdFile &msd, TimingData &out ); + bool LoadEditFromFile( RString sEditFilePath, ProfileSlot slot, bool bAddStepsToSong ); bool LoadEditFromBuffer( const RString &sBuffer, const RString &sEditFilePath, ProfileSlot slot ); bool LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePath, ProfileSlot slot, bool bAddStepsToSong ); bool LoadFromBGChangesString( BackgroundChange &change, const RString &sBGChangeExpression ); + + void ProcessBeatsPerMeasure( TimingData &out, const RString sParam ); + bool ProcessBPMs( TimingData &out, const int iRowsPerBeat, const RString sParam ); + void ProcessStops( TimingData &out, const int iRowsPerBeat, const RString sParam ); + void ProcessDelays( TimingData &out, const int iRowsPerBeat, const RString sParam ); + void ProcessTickcounts( TimingData &out, const int iRowsPerBeat, const RString sParam ); + void ProcessMultipliers( TimingData &out, const int iRowsPerBeat, const RString sParam ); + void ProcessSpeeds( TimingData &out, const int iRowsPerBeat, const RString sParam ); + void ProcessFakes( TimingData &out, const int iRowsPerBeat, const RString sParam ); + + float RowToBeat( RString sLine, const int iRowsPerBeat ); }; #endif diff --git a/src/NotesLoaderSSC.cpp b/src/NotesLoaderSSC.cpp index 217b31507a..81969e336e 100644 --- a/src/NotesLoaderSSC.cpp +++ b/src/NotesLoaderSSC.cpp @@ -4,7 +4,7 @@ #include "GameManager.h" #include "MsdFile.h" // No JSON here. #include "NoteTypes.h" -#include "NotesLoaderSM.h" // For loading SM style edits. +#include "NotesLoaderSM.h" // For programming shortcuts. #include "RageFileManager.h" #include "RageLog.h" #include "RageUtil.h" @@ -21,79 +21,13 @@ const int MAX_EDIT_STEPS_SIZE_BYTES = 60*1024; // 60 KB /** * @brief Attempt to load any background changes in use by this song. - * - * This code is right now copied from NotesLoaderSM. There may be a time - * when we add to this code, or perhaps just refactor it properly. * @param change a reference to the background change. * @param sBGChangeExpression a reference to the list of changes to be made. * @return its success or failure. */ bool LoadFromBGSSCChangesString( BackgroundChange &change, const RString &sBGChangeExpression ) { - vector aBGChangeValues; - split( sBGChangeExpression, "=", aBGChangeValues, false ); - - aBGChangeValues.resize( min((int)aBGChangeValues.size(),11) ); - - switch( aBGChangeValues.size() ) - { - case 11: - change.m_def.m_sColor2 = aBGChangeValues[10]; - change.m_def.m_sColor2.Replace( '^', ',' ); - change.m_def.m_sColor2 = RageColor::NormalizeColorString( change.m_def.m_sColor2 ); - // fall through - case 10: - change.m_def.m_sColor1 = aBGChangeValues[9]; - change.m_def.m_sColor1.Replace( '^', ',' ); - change.m_def.m_sColor1 = RageColor::NormalizeColorString( change.m_def.m_sColor1 ); - // fall through - case 9: - change.m_sTransition = aBGChangeValues[8]; - // fall through - case 8: - change.m_def.m_sFile2 = aBGChangeValues[7]; - // fall through - case 7: - change.m_def.m_sEffect = aBGChangeValues[6]; - // fall through - case 6: - // param 7 overrides this. - // Backward compatibility: - if( change.m_def.m_sEffect.empty() ) - { - bool bLoop = StringToInt( aBGChangeValues[5] ) != 0; - if( !bLoop ) - change.m_def.m_sEffect = SBE_StretchNoLoop; - } - // fall through - case 5: - // param 7 overrides this. - // Backward compatibility: - if( change.m_def.m_sEffect.empty() ) - { - bool bRewindMovie = StringToInt( aBGChangeValues[4] ) != 0; - if( bRewindMovie ) - change.m_def.m_sEffect = SBE_StretchRewind; - } - // fall through - case 4: - // param 9 overrides this. - // Backward compatibility: - if( change.m_sTransition.empty() ) - change.m_sTransition = (StringToInt( aBGChangeValues[3] ) != 0) ? "CrossFade" : ""; - // fall through - case 3: - change.m_fRate = StringToFloat( aBGChangeValues[2] ); - // fall through - case 2: - change.m_def.m_sFile1 = aBGChangeValues[1]; - // fall through - case 1: - change.m_fStartBeat = StringToFloat( aBGChangeValues[0] ); - // fall through - } - - return aBGChangeValues.size() >= 2; + return SMLoader::LoadFromBGChangesString( change, sBGChangeExpression ); } bool SSCLoader::LoadFromDir( const RString &sPath, Song &out ) @@ -112,6 +46,166 @@ bool SSCLoader::LoadFromDir( const RString &sPath, Song &out ) return LoadFromSSCFile( sPath + aFileNames[0], out ); } +void SSCLoader::ProcessWarps( TimingData &out, const RString sParam, const float fVersion ) +{ + vector arrayWarpExpressions; + split( sParam, ",", arrayWarpExpressions ); + + for( unsigned b=0; b arrayWarpValues; + split( arrayWarpExpressions[b], "=", arrayWarpValues ); + // XXX: Hard to tell which file caused this. + if( arrayWarpValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #WARPS value \"%s\" (must have exactly one '='), ignored.", + arrayWarpExpressions[b].c_str() ); + continue; + } + + const float fBeat = StringToFloat( arrayWarpValues[0] ); + const float fNewBeat = StringToFloat( arrayWarpValues[1] ); + // Early versions were absolute in beats. They should be relative. + if( ( fVersion < VERSION_SPLIT_TIMING && fNewBeat > fBeat ) ) + { + out.AddWarpSegment( WarpSegment(fBeat, fNewBeat - fBeat) ); + } + else if( fNewBeat > 0 ) + out.AddWarpSegment( WarpSegment(fBeat, fNewBeat) ); + else + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid Warp at beat %f, BPM %f.", fBeat, fNewBeat ); + } + } +} + +void SSCLoader::ProcessLabels( TimingData &out, const RString sParam ) +{ + vector arrayLabelExpressions; + split( sParam, ",", arrayLabelExpressions ); + + for( unsigned b=0; b arrayLabelValues; + split( arrayLabelExpressions[b], "=", arrayLabelValues ); + if( arrayLabelValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #LABELS value \"%s\" (must have exactly one '='), ignored.", + arrayLabelExpressions[b].c_str() ); + continue; + } + + const float fBeat = StringToFloat( arrayLabelValues[0] ); + RString sLabel = arrayLabelValues[1]; + TrimRight(sLabel); + if( fBeat >= 0.0f ) + out.AddLabelSegment( LabelSegment(fBeat, sLabel) ); + else + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid Label at beat %f called %s.", fBeat, sLabel.c_str() ); + } + + } +} + +void SSCLoader::ProcessCombos( TimingData &out, const RString sParam ) +{ + vector arrayComboExpressions; + split( sParam, ",", arrayComboExpressions ); + + for( unsigned f=0; f arrayComboValues; + split( arrayComboExpressions[f], "=", arrayComboValues ); + if( arrayComboValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #COMBOS value \"%s\" (must have exactly one '='), ignored.", + arrayComboExpressions[f].c_str() ); + continue; + } + const float fComboBeat = StringToFloat( arrayComboValues[0] ); + const int iCombos = StringToInt( arrayComboValues[1] ); + ComboSegment new_seg( BeatToNoteRow( fComboBeat ), iCombos ); + out.AddComboSegment( new_seg ); + } +} + +void SSCLoader::ProcessSpeeds( TimingData &out, const RString sParam ) +{ + vector vs1; + split( sParam, ",", vs1 ); + + FOREACH_CONST( RString, vs1, s1 ) + { + vector vs2; + split( *s1, "=", vs2 ); + + if( vs2[0] == 0 && vs2.size() == 2 ) // First one always seems to have 2. + { + vs2.push_back("0"); + } + + if( vs2.size() == 3 ) // use beats by default. + { + vs2.push_back("0"); + } + + if( vs2.size() < 4 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an speed change with %i values.", (int)vs2.size() ); + continue; + } + + const float fBeat = StringToFloat( vs2[0] ); + + SpeedSegment seg( fBeat, StringToFloat( vs2[1] ), StringToFloat( vs2[2] ), static_cast(StringToInt(vs2[3]))); + + if( fBeat < 0 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an speed change with beat %f.", fBeat ); + continue; + } + + if( seg.m_fWait < 0 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an speed change with beat %f, fWait %f.", fBeat, seg.m_fWait ); + continue; + } + + out.AddSpeedSegment( seg ); + } +} + +void SSCLoader::ProcessFakes( TimingData &out, const RString sParam ) +{ + vector arrayFakeExpressions; + split( sParam, ",", arrayFakeExpressions ); + + for( unsigned b=0; b arrayFakeValues; + split( arrayFakeExpressions[b], "=", arrayFakeValues ); + // XXX: Hard to tell which file caused this. + if( arrayFakeValues.size() != 2 ) + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #FAKES value \"%s\" (must have exactly one '='), ignored.", + arrayFakeExpressions[b].c_str() ); + continue; + } + + const float fBeat = StringToFloat( arrayFakeValues[0] ); + const float fNewBeat = StringToFloat( arrayFakeValues[1] ); + + if(fNewBeat > 0) + out.AddFakeSegment( FakeSegment(fBeat, fNewBeat) ); + else + { + LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid Fake at beat %f, BPM %f.", fBeat, fNewBeat ); + } + } +} + + bool SSCLoader::LoadFromSSCFile( const RString &sPath, Song &out, bool bFromCache ) { LOG->Trace( "Song::LoadFromSSCFile(%s)", sPath.c_str() ); @@ -123,11 +217,13 @@ bool SSCLoader::LoadFromSSCFile( const RString &sPath, Song &out, bool bFromCach return false; } - out.m_Timing.m_sFile = sPath; // songs still have their fallback timing. + out.m_SongTiming.m_sFile = sPath; // songs still have their fallback timing. int state = GETTING_SONG_INFO; const unsigned values = msd.GetNumValues(); Steps* pNewNotes = NULL; + TimingData stepsTiming; + bool bHasOwnTiming = false; for( unsigned i = 0; i < values; i++ ) { @@ -216,19 +312,7 @@ bool SSCLoader::LoadFromSSCFile( const RString &sPath, Song &out, bool bFromCach else if( sValueName=="INSTRUMENTTRACK" ) { - vector vs1; - split( sParams[1], ",", vs1 ); - FOREACH_CONST( RString, vs1, s ) - { - vector vs2; - split( *s, "=", vs2 ); - if( vs2.size() >= 2 ) - { - InstrumentTrack it = StringToInstrumentTrack( vs2[0] ); - if( it != InstrumentTrack_Invalid ) - out.m_sInstrumentTrackFile[it] = vs2[1]; - } - } + SMLoader::ProcessInstrumentTracks( out, sParams[1] ); } else if( sValueName=="MUSICLENGTH" ) @@ -295,27 +379,7 @@ bool SSCLoader::LoadFromSSCFile( const RString &sPath, Song &out, bool bFromCach else if( sValueName.Left(strlen("BGCHANGES"))=="BGCHANGES" || sValueName=="ANIMATIONS" ) { - BackgroundLayer iLayer = BACKGROUND_LAYER_1; - if( sscanf(sValueName, "BGCHANGES%d", &*ConvertValue(&iLayer)) == 1 ) - enum_add(iLayer, -1); // #BGCHANGES2 = BACKGROUND_LAYER_2 - - bool bValid = iLayer>=0 && iLayerUserLog( "Song file", sPath, "has a #BGCHANGES tag \"%s\" that is out of range.", sValueName.c_str() ); - } - else - { - vector aBGChangeExpressions; - split( sParams[1], ",", aBGChangeExpressions ); - - for( unsigned b=0; b sBits; - split( sParams[j], "=", sBits, false ); - - // Need an identifer and a value for this to work - if( sBits.size() < 2 ) - continue; - - TrimLeft( sBits[0] ); - TrimRight( sBits[0] ); - - if( !sBits[0].CompareNoCase("TIME") ) - attack.fStartSecond = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("LEN") ) - attack.fSecsRemaining = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("END") ) - end = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("MODS") ) - { - attack.sModifiers = sBits[1]; - - if( end != -9999 ) - { - attack.fSecsRemaining = end - attack.fStartSecond; - end = -9999; - } - - if( attack.fSecsRemaining < 0.0f ) - attack.fSecsRemaining = 0.0f; - - out.m_Attacks.push_back( attack ); - } - } + SMLoader::ProcessAttacks( out, sParams ); } else if( sValueName=="OFFSET" ) { - out.m_Timing.m_fBeat0OffsetInSeconds = StringToFloat( sParams[1] ); + out.m_SongTiming.m_fBeat0OffsetInSeconds = StringToFloat( sParams[1] ); } /* Below are the song based timings that should only be used * if the steps do not have their own timing. */ else if( sValueName=="STOPS" ) { - vector arrayFreezeExpressions; - split( sParams[1], ",", arrayFreezeExpressions ); - - for( unsigned f=0; f arrayFreezeValues; - split( arrayFreezeExpressions[f], "=", arrayFreezeValues ); - if( arrayFreezeValues.size() != 2 ) - { - // XXX: Hard to tell which file caused this. - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayFreezeExpressions[f].c_str() ); - continue; - } - - const float fFreezeBeat = StringToFloat( arrayFreezeValues[0] ); - const float fFreezeSeconds = StringToFloat( arrayFreezeValues[1] ); - StopSegment new_seg( BeatToNoteRow(fFreezeBeat), fFreezeSeconds ); - - if(fFreezeSeconds > 0.0f) - { - // LOG->Trace( "Adding a freeze segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); - out.m_Timing.AddStopSegment( new_seg ); - } - else - { - // negative stops (hi JS!) -aj - if( PREFSMAN->m_bQuirksMode ) - { - // LOG->Trace( "Adding a negative freeze segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); - out.m_Timing.AddStopSegment( new_seg ); - } - else - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid stop at beat %f, length %f.", fFreezeBeat, fFreezeSeconds ); - } - } + SMLoader::ProcessStops(out.m_SongTiming, sParams[1]); } else if( sValueName=="DELAYS" ) { - vector arrayDelayExpressions; - split( sParams[1], ",", arrayDelayExpressions ); - - for( unsigned f=0; f arrayDelayValues; - split( arrayDelayExpressions[f], "=", arrayDelayValues ); - if( arrayDelayValues.size() != 2 ) - { - // XXX: Hard to tell which file caused this. - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayDelayExpressions[f].c_str() ); - continue; - } - - const float fFreezeBeat = StringToFloat( arrayDelayValues[0] ); - const float fFreezeSeconds = StringToFloat( arrayDelayValues[1] ); - - StopSegment new_seg( BeatToNoteRow(fFreezeBeat), fFreezeSeconds, true ); - - // LOG->Trace( "Adding a delay segment: beat: %f, seconds = %f", new_seg.m_fStartBeat, new_seg.m_fStopSeconds ); - - if(fFreezeSeconds > 0.0f) - out.m_Timing.AddStopSegment( new_seg ); - else - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid delay at beat %f, length %f.", fFreezeBeat, fFreezeSeconds ); - } + SMLoader::ProcessDelays(out.m_SongTiming, sParams[1]); } else if( sValueName=="BPMS" ) { - vector arrayBPMChangeExpressions; - split( sParams[1], ",", arrayBPMChangeExpressions ); - - for( unsigned b=0; b arrayBPMChangeValues; - split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); - // XXX: Hard to tell which file caused this. - if( arrayBPMChangeValues.size() != 2 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayBPMChangeExpressions[b].c_str() ); - continue; - } - - const float fBeat = StringToFloat( arrayBPMChangeValues[0] ); - const float fNewBPM = StringToFloat( arrayBPMChangeValues[1] ); - - if(fNewBPM > 0.0f) - out.m_Timing.AddBPMSegment( BPMSegment(BeatToNoteRow(fBeat), fNewBPM) ); - else - { - out.m_Timing.m_bHasNegativeBpms = true; - // only add Negative BPMs in quirks mode -aj - if( PREFSMAN->m_bQuirksMode ) - out.m_Timing.AddBPMSegment( BPMSegment(BeatToNoteRow(fBeat), fNewBPM) ); - else - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid BPM change at beat %f, BPM %f.", fBeat, fNewBPM ); - } - } + SMLoader::ProcessBPMs(out.m_SongTiming, sParams[1]); } - else if( sValueName=="WARPS" ) + else if( sValueName=="WARPS" ) // Older versions allowed em here. { - vector arrayWarpExpressions; - split( sParams[1], ",", arrayWarpExpressions ); - - for( unsigned b=0; b arrayWarpValues; - split( arrayWarpExpressions[b], "=", arrayWarpValues ); - // XXX: Hard to tell which file caused this. - if( arrayWarpValues.size() != 2 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayWarpExpressions[b].c_str() ); - continue; - } - - const float fBeat = StringToFloat( arrayWarpValues[0] ); - const float fNewBeat = StringToFloat( arrayWarpValues[1] ); - - if(fNewBeat > fBeat) - out.m_Timing.AddWarpSegment( WarpSegment(fBeat, fNewBeat) ); - else - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid Warp at beat %f, BPM %f.", fBeat, fNewBeat ); - } - } + ProcessWarps( out.m_SongTiming, sParams[1], out.m_fVersion ); } else if( sValueName=="LABELS" ) { - vector arrayLabelExpressions; - split( sParams[1], ",", arrayLabelExpressions ); - - for( unsigned b=0; b arrayLabelValues; - split( arrayLabelExpressions[b], "=", arrayLabelValues ); - if( arrayLabelValues.size() != 2 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayLabelExpressions[b].c_str() ); - continue; - } - - const float fBeat = StringToFloat( arrayLabelValues[0] ); - RString sLabel = arrayLabelValues[1]; - TrimRight(sLabel); - if( fBeat >= 0.0f ) - out.m_Timing.AddLabelSegment( LabelSegment(fBeat, sLabel) ); - else - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid Label at beat %f called %s.", fBeat, sLabel.c_str() ); - } - - } + ProcessLabels( out.m_SongTiming, sParams[1] ); } else if( sValueName=="TIMESIGNATURES" ) { - vector vs1; - split( sParams[1], ",", vs1 ); - - FOREACH_CONST( RString, vs1, s1 ) - { - vector vs2; - split( *s1, "=", vs2 ); - - if( vs2.size() < 3 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with %i values.", (int)vs2.size() ); - continue; - } - - const float fBeat = StringToFloat( vs2[0] ); - - TimeSignatureSegment seg; - seg.m_iStartRow = BeatToNoteRow(fBeat); - seg.m_iNumerator = StringToInt( vs2[1] ); - seg.m_iDenominator = StringToInt( vs2[2] ); - - if( fBeat < 0 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f.", fBeat ); - continue; - } - - if( seg.m_iNumerator < 1 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f, iNumerator %i.", fBeat, seg.m_iNumerator ); - continue; - } - - if( seg.m_iDenominator < 1 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid time signature change with beat %f, iDenominator %i.", fBeat, seg.m_iDenominator ); - continue; - } - - out.m_Timing.AddTimeSignatureSegment( seg ); - } + SMLoader::ProcessTimeSignatures(out.m_SongTiming, sParams[1]); } else if( sValueName=="TICKCOUNTS" ) { - vector arrayTickcountExpressions; - split( sParams[1], ",", arrayTickcountExpressions ); - - for( unsigned f=0; f arrayTickcountValues; - split( arrayTickcountExpressions[f], "=", arrayTickcountValues ); - if( arrayTickcountValues.size() != 2 ) - { - // XXX: Hard to tell which file caused this. - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayTickcountExpressions[f].c_str() ); - continue; - } - - const float fTickcountBeat = StringToFloat( arrayTickcountValues[0] ); - const int iTicks = StringToInt( arrayTickcountValues[1] ); - TickcountSegment new_seg( BeatToNoteRow(fTickcountBeat), iTicks ); - - if(iTicks >= 1 && iTicks <= ROWS_PER_BEAT ) // Constants - { - // LOG->Trace( "Adding a tickcount segment: beat: %f, ticks = %d", fTickcountBeat, iTicks ); - out.m_Timing.AddTickcountSegment( new_seg ); - } - else - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid tickcount at beat %f, ticks %d.", fTickcountBeat, iTicks ); - } - } + SMLoader::ProcessTickcounts(out.m_SongTiming, sParams[1]); } else if( sValueName=="COMBOS" ) { - vector arrayComboExpressions; - split( sParams[1], ",", arrayComboExpressions ); - - for( unsigned f=0; f arrayComboValues; - split( arrayComboExpressions[f], "=", arrayComboValues ); - if( arrayComboValues.size() != 2 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayComboExpressions[f].c_str() ); - continue; - } - const float fComboBeat = StringToFloat( arrayComboValues[0] ); - const int iCombos = StringToInt( arrayComboValues[1] ); - ComboSegment new_seg( BeatToNoteRow( fComboBeat ), iCombos ); - out.m_Timing.AddComboSegment( new_seg ); - } + ProcessCombos( out.m_SongTiming, sParams[1] ); } /* The following are cache tags. Never fill their values @@ -686,7 +487,9 @@ bool SSCLoader::LoadFromSSCFile( const RString &sPath, Song &out, bool bFromCach else if( sValueName=="NOTEDATA" ) { state = GETTING_STEP_INFO; - pNewNotes = new Steps; + pNewNotes = out.CreateSteps(); + stepsTiming = TimingData( out.m_SongTiming.m_fBeat0OffsetInSeconds ); + bHasOwnTiming = false; } break; } @@ -750,143 +553,79 @@ bool SSCLoader::LoadFromSSCFile( const RString &sPath, Song &out, bool bFromCach else if( sValueName=="NOTES" || sValueName=="NOTES2" ) { state = GETTING_SONG_INFO; - //pNewNotes->m_Timing = out.m_Timing; + if( bHasOwnTiming ) + pNewNotes->m_Timing = stepsTiming; pNewNotes->SetSMNoteData( sParams[1] ); pNewNotes->TidyUpData(); out.AddSteps( pNewNotes ); } - + else if( sValueName=="BPMS" ) { - /* - state = GETTING_STEP_TIMING_INFO; - vector arrayBPMChangeExpressions; - split( sParams[1], ",", arrayBPMChangeExpressions ); - - for( unsigned b=0; b arrayBPMChangeValues; - split( arrayBPMChangeExpressions[b], "=", arrayBPMChangeValues ); - // XXX: Hard to tell which file caused this. - if( arrayBPMChangeValues.size() != 2 ) - { - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid #%s value \"%s\" (must have exactly one '='), ignored.", - sValueName.c_str(), arrayBPMChangeExpressions[b].c_str() ); - continue; - } - - const float fBeat = StringToFloat( arrayBPMChangeValues[0] ); - const float fNewBPM = StringToFloat( arrayBPMChangeValues[1] ); - - if(fNewBPM > 0.0f) - pNewNotes->m_Timing.AddBPMSegment( BPMSegment(BeatToNoteRow(fBeat), fNewBPM) ); - else - { - pNewNotes->m_Timing.m_bHasNegativeBpms = true; - // only add Negative BPMs in quirks mode -aj - if( PREFSMAN->m_bQuirksMode ) - pNewNotes->m_Timing.AddBPMSegment( BPMSegment(BeatToNoteRow(fBeat), fNewBPM) ); - else - LOG->UserLog( "Song file", "(UNKNOWN)", "has an invalid BPM change at beat %f, BPM %f.", fBeat, fNewBPM ); - } - } - */ + if( SMLoader::ProcessBPMs(stepsTiming, sParams[1]) ) + bHasOwnTiming = true; } - break; - } - case GETTING_STEP_TIMING_INFO: - { - if( sValueName=="STOPS" ) + + else if( sValueName=="STOPS" ) { - // copy from above when it's time. + SMLoader::ProcessStops(stepsTiming, sParams[1]); } + else if( sValueName=="DELAYS" ) { - // copy from above when it's time. + SMLoader::ProcessDelays(stepsTiming, sParams[1]); } + else if( sValueName=="TIMESIGNATURES" ) { - // copy from above when it's time. + SMLoader::ProcessTimeSignatures(stepsTiming, sParams[1]); } - + else if( sValueName=="TICKCOUNTS" ) { - // copy from above when it's time. + SMLoader::ProcessTickcounts(stepsTiming, sParams[1]); } + else if( sValueName=="COMBOS" ) { - // copy from above when it's time. + ProcessCombos(stepsTiming, sParams[1]); } - else if( sValueName=="WARPS" || sValueName=="LABELS" ) + + else if( sValueName=="WARPS" ) { - // copy from above when it's time. + ProcessWarps(stepsTiming, sParams[1], out.m_fVersion); } + + else if( sValueName=="SPEEDS" ) + { + ProcessSpeeds( stepsTiming, sParams[1] ); + } + + else if( sValueName=="FAKES" ) + { + ProcessFakes( stepsTiming, sParams[1] ); + } + + else if( sValueName=="LABELS" ) + { + ProcessLabels(stepsTiming, sParams[1]); + } + else if( sValueName=="ATTACKS" ) { - // TODO: Look into Step attacks vs Song Attacks. -Wolfman2000 - /* - // Build the RString vector here so we can write it to file again later - for( unsigned s=1; s < sParams.params.size(); ++s ) - out.m_sAttackString.push_back( sParams[s] ); - - Attack attack; - float end = -9999; - - for( unsigned j=1; j < sParams.params.size(); ++j ) - { - vector sBits; - split( sParams[j], "=", sBits, false ); - - // Need an identifer and a value for this to work - if( sBits.size() < 2 ) - continue; - - TrimLeft( sBits[0] ); - TrimRight( sBits[0] ); - - if( !sBits[0].CompareNoCase("TIME") ) - attack.fStartSecond = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("LEN") ) - attack.fSecsRemaining = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("END") ) - end = strtof( sBits[1], NULL ); - else if( !sBits[0].CompareNoCase("MODS") ) - { - attack.sModifiers = sBits[1]; - - if( end != -9999 ) - { - attack.fSecsRemaining = end - attack.fStartSecond; - end = -9999; - } - - if( attack.fSecsRemaining < 0.0f ) - attack.fSecsRemaining = 0.0f; - - out.m_Attacks.push_back( attack ); - } - } - */ + // Step Attacks aren't in yet. } + else if( sValueName=="OFFSET" ) - {/* - pNewNotes->m_Timing.m_fBeat0OffsetInSeconds = StringToFloat( sParams[1] ); - */ - } - - else if( sValueName=="NOTES" ) { - state = GETTING_SONG_INFO; - // pNewNotes->m_Timing.m_fBeat0OffsetInSeconds = out.m_Timing.m_fBeat0OffsetInSeconds; - pNewNotes->SetSMNoteData( sParams[1] ); - pNewNotes->TidyUpData(); - out.AddSteps( pNewNotes ); + stepsTiming.m_fBeat0OffsetInSeconds = StringToFloat( sParams[1] ); } break; } } } out.m_fVersion = STEPFILE_VERSION_NUMBER; + TidyUpData(out, bFromCache); return true; } @@ -921,6 +660,8 @@ bool SSCLoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePat Song* pSong = NULL; Steps* pNewNotes = NULL; bool bSSCFormat = false; + bool bHasOwnTiming = false; + TimingData stepsTiming; for( unsigned i=0; im_fVersion = StringToFloat( sParams[1] ); + } + + else if( sValueName=="SONG" ) { if( pSong ) { @@ -957,7 +703,7 @@ bool SSCLoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePat else if( sValueName=="NOTEDATA" ) { - pNewNotes = new Steps; + pNewNotes = pSong->CreateSteps(); bSSCFormat = true; } if( sValueName=="STEPSTYPE" ) @@ -1011,32 +757,66 @@ bool SSCLoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePat bSSCFormat = true; } - // TimingData for Steps isn't set yet, but still prepare for it. else if( sValueName=="BPMS" ) { + if( SMLoader::ProcessBPMs(stepsTiming, sParams[1]) ) + bHasOwnTiming = true; bSSCFormat = true; } + else if( sValueName=="STOPS" ) { + SMLoader::ProcessStops(stepsTiming, sParams[1]); bSSCFormat = true; } + else if( sValueName=="DELAYS" ) { + SMLoader::ProcessDelays(stepsTiming, sParams[1]); bSSCFormat = true; } + else if( sValueName=="TIMESIGNATURES" ) { + SMLoader::ProcessTimeSignatures(stepsTiming, sParams[1]); bSSCFormat = true; } + else if( sValueName=="TICKCOUNTS" ) { + SMLoader::ProcessTickcounts(stepsTiming, sParams[1]); bSSCFormat = true; } + else if( sValueName=="COMBOS" ) { + ProcessCombos(stepsTiming, sParams[1]); + bSSCFormat = true; + } + + else if( sValueName=="WARPS" ) + { + ProcessWarps(stepsTiming, sParams[1], pSong->m_fVersion); + bSSCFormat = true; + } + + else if( sValueName=="SPEEDS" ) + { + ProcessSpeeds( stepsTiming, sParams[1] ); + bSSCFormat = true; + } + + else if( sValueName=="FAKES" ) + { + ProcessFakes( stepsTiming, sParams[1] ); + bSSCFormat = true; + } + + else if( sValueName=="LABELS" ) + { + ProcessLabels(stepsTiming, sParams[1]); bSSCFormat = true; } - else if( sValueName=="NOTES" ) { if( pSong == NULL ) @@ -1056,13 +836,17 @@ bool SSCLoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePat if( bSSCFormat ) { + if ( bHasOwnTiming ) + { + pNewNotes->m_Timing = stepsTiming; + } pNewNotes->SetSMNoteData( sParams[1] ); pNewNotes->TidyUpData(); pSong->AddSteps( pNewNotes ); } else { - pNewNotes = new Steps; + pNewNotes = pSong->CreateSteps(); SMLoader::LoadFromSMTokens( sParams[1], sParams[2], sParams[3], sParams[4], sParams[5], sParams[6], *pNewNotes); @@ -1095,55 +879,7 @@ bool SSCLoader::LoadEditFromMsd( const MsdFile &msd, const RString &sEditFilePat void SSCLoader::TidyUpData( Song &song, bool bFromCache ) { - /* - * Hack: if the song has any changes at all (so it won't use a random BGA) - * and doesn't end with "-nosongbg-", add a song background BGC. Remove - * "-nosongbg-" if it exists. - * - * This way, songs that were created earlier, when we added the song BG - * at the end by default, will still behave as expected; all new songs will - * have to add an explicit song BG tag if they want it. This is really a - * formatting hack only; nothing outside of SMLoader ever sees "-nosongbg-". - */ - vector &bg = song.GetBackgroundChanges(BACKGROUND_LAYER_1); - if( !bg.empty() ) - { - /* BGChanges have been sorted. On the odd chance that a BGChange exists - * with a very high beat, search the whole list. */ - bool bHasNoSongBgTag = false; - - for( unsigned i = 0; !bHasNoSongBgTag && i < bg.size(); ++i ) - { - if( !bg[i].m_def.m_sFile1.CompareNoCase(NO_SONG_BG_FILE) ) - { - bg.erase( bg.begin()+i ); - bHasNoSongBgTag = true; - } - } - - // If there's no -nosongbg- tag, add the song BG. - if( !bHasNoSongBgTag ) do - { - /* If we're loading cache, -nosongbg- should always be in there. We - * must not call IsAFile(song.GetBackgroundPath()) when loading cache. */ - if( bFromCache ) - break; - - /* If BGChanges already exist after the last beat, don't add the - * background in the middle. */ - if( !bg.empty() && bg.back().m_fStartBeat-0.0001f >= song.m_fLastBeat ) - break; - - // If the last BGA is already the song BGA, don't add a duplicate. - if( !bg.empty() && !bg.back().m_def.m_sFile1.CompareNoCase(song.m_sBackgroundFile) ) - break; - - if( !IsAFile( song.GetBackgroundPath() ) ) - break; - - bg.push_back( BackgroundChange(song.m_fLastBeat,song.m_sBackgroundFile) ); - } while(0); - } + SMLoader::TidyUpData(song, bFromCache); } /* diff --git a/src/NotesLoaderSSC.h b/src/NotesLoaderSSC.h index ff44a23cb5..cccf26c45f 100644 --- a/src/NotesLoaderSSC.h +++ b/src/NotesLoaderSSC.h @@ -16,8 +16,6 @@ enum SSCLoadingStates { GETTING_SONG_INFO, /**< Retrieving song information. */ GETTING_STEP_INFO, /**< Retrieving step information. */ - GETTING_STEP_TIMING_INFO, /**< Retrieving a step's individual timing information. */ - GETTING_NOTE_INFO, /**< Retrieving the specific notes. This state may be deprecated. */ NUM_SSCLoadingStates /**< The number of states used. */ }; @@ -25,6 +23,8 @@ enum SSCLoadingStates const float VERSION_RADAR_FAKE = 0.53f; /** @brief The version where WarpSegments started to be utilized. */ const float VERSION_WARP_SEGMENT = 0.56f; +/** @brief The version that formally introduced Split Timing. */ +const float VERSION_SPLIT_TIMING = 0.7f; /** * @brief The SSCLoader handles all of the parsing needed for .ssc files. @@ -75,6 +75,13 @@ namespace SSCLoader * @param bFromCache a flag to determine if this song is loaded from a cache file. */ void TidyUpData( Song &song, bool bFromCache ); + + + void ProcessWarps( TimingData &, const RString, const float ); + void ProcessLabels( TimingData &, const RString ); + void ProcessCombos( TimingData &, const RString ); + void ProcessSpeeds( TimingData &, const RString ); + void ProcessFakes( TimingData &, const RString ); } #endif /** diff --git a/src/NotesWriterDWI.cpp b/src/NotesWriterDWI.cpp index ce8ba00231..260728a70d 100644 --- a/src/NotesWriterDWI.cpp +++ b/src/NotesWriterDWI.cpp @@ -350,10 +350,10 @@ bool NotesWriterDWI::Write( RString sPath, const Song &out ) /* Write transliterations, if we have them, since DWI doesn't support UTF-8. */ f.PutLine( ssprintf("#TITLE:%s;", DwiEscape(out.GetTranslitFullTitle()).c_str()) ); f.PutLine( ssprintf("#ARTIST:%s;", DwiEscape(out.GetTranslitArtist()).c_str()) ); - ASSERT( out.m_Timing.m_BPMSegments[0].m_iStartRow == 0 ); + ASSERT( out.m_SongTiming.m_BPMSegments[0].m_iStartRow == 0 ); f.PutLine( ssprintf("#FILE:%s;", DwiEscape(out.m_sMusicFile).c_str()) ); - f.PutLine( ssprintf("#BPM:%.3f;", out.m_Timing.m_BPMSegments[0].GetBPM()) ); - f.PutLine( ssprintf("#GAP:%ld;", -lrintf( out.m_Timing.m_fBeat0OffsetInSeconds*1000 )) ); + f.PutLine( ssprintf("#BPM:%.3f;", out.m_SongTiming.m_BPMSegments[0].GetBPM()) ); + f.PutLine( ssprintf("#GAP:%ld;", -lrintf( out.m_SongTiming.m_fBeat0OffsetInSeconds*1000 )) ); f.PutLine( ssprintf("#SAMPLESTART:%.3f;", out.m_fMusicSampleStartSeconds) ); f.PutLine( ssprintf("#SAMPLELENGTH:%.3f;", out.m_fMusicSampleLengthSeconds) ); if( out.m_sCDTitleFile.size() ) @@ -374,29 +374,29 @@ bool NotesWriterDWI::Write( RString sPath, const Song &out ) break; } - if( !out.m_Timing.m_StopSegments.empty() ) + if( !out.m_SongTiming.m_StopSegments.empty() ) { f.Write( "#FREEZE:" ); - for( unsigned i=0; i 1) + if( out.m_SongTiming.m_BPMSegments.size() > 1) { f.Write( "#CHANGEBPM:" ); - for( unsigned i=1; i vs; - FOREACH_ENUM( InstrumentTrack, it ) - if( out.HasInstrumentTrack(it) ) - vs.push_back( InstrumentTrackToString(it) + - "=" + out.m_sInstrumentTrackFile[it] ); - if( !vs.empty() ) - { - RString s = join( ",", vs ); - f.PutLine( "#INSTRUMENTTRACK:" + s + ";\n" ); - } - } - f.PutLine( ssprintf( "#OFFSET:%.3f;", out.m_Timing.m_fBeat0OffsetInSeconds ) ); + f.PutLine( ssprintf( "#OFFSET:%.3f;", out.m_SongTiming.m_fBeat0OffsetInSeconds ) ); f.PutLine( ssprintf( "#SAMPLESTART:%.3f;", out.m_fMusicSampleStartSeconds ) ); f.PutLine( ssprintf( "#SAMPLELENGTH:%.3f;", out.m_fMusicSampleLengthSeconds ) ); if( out.m_fSpecifiedLastBeat > 0 ) @@ -111,82 +98,55 @@ static void WriteGlobalTags( RageFile &f, const Song &out ) f.Write( "#BPMS:" ); - for( unsigned i=0; i 0 ) + { + for( unsigned i=0; i < wSize; i++ ) + { + int iRow = out.m_SongTiming.m_WarpSegments[i].m_iStartRow; + float fBPS = 60 / out.m_SongTiming.GetBPMAtRow(iRow); + float fSkip = fBPS * out.m_SongTiming.m_WarpSegments[i].m_fLengthBeats; + StopSegment ss; + ss.m_iStartRow = iRow; + ss.m_fStopSeconds = -fSkip; + ss.m_bDelay = false; // Best to be sure. + out.m_SongTiming.AddStopSegment( ss ); + } + } f.Write( "#STOPS:" ); - for( unsigned i=0; im_iStartRow), - iter->m_iNumerator, iter->m_iDenominator ) ); - vector::const_iterator iter2 = iter; - iter2++; - if( iter2 != out.m_Timing.m_vTimeSignatureSegments.end() ) - f.Write( "," ); - } - f.PutLine( ";" ); - - ASSERT( !out.m_Timing.m_TickcountSegments.empty() ); - f.Write( "#TICKCOUNTS:" ); - for( unsigned i=0; i asRadarValues; - // SM files don't use fakes for radar data. Keep it that way. - int categories = NUM_RadarCategory - 1; + // OpenITG simfiles use 11 radar categories. + int categories = 11; FOREACH_PlayerNumber( pn ) { const RadarValues &rv = in.GetRadarValues( pn ); @@ -300,7 +260,7 @@ static RString GetSMNotesTag( const Song &song, const Steps &in ) return JoinLineList( lines ); } -bool NotesWriterSM::Write( RString sPath, const Song &out, const vector& vpStepsToSave ) +bool NotesWriterSM::Write( RString sPath, Song &out, const vector& vpStepsToSave ) { int flags = RageFile::WRITE; diff --git a/src/NotesWriterSM.h b/src/NotesWriterSM.h index ee0ed26441..580953cdac 100644 --- a/src/NotesWriterSM.h +++ b/src/NotesWriterSM.h @@ -11,7 +11,7 @@ namespace NotesWriterSM * @param sPath the path to write the file. * @param out the Song to be written out. * @return its success or failure. */ - bool Write( RString sPath, const Song &out, const vector& vpStepsToSave ); + bool Write( RString sPath, Song &out, const vector& vpStepsToSave ); /** * @brief Get some contents about the edit file first. * @param pSong the Song in question. diff --git a/src/NotesWriterSSC.cpp b/src/NotesWriterSSC.cpp index 69ca696756..77578908f8 100644 --- a/src/NotesWriterSSC.cpp +++ b/src/NotesWriterSSC.cpp @@ -42,7 +42,128 @@ static RString BackgroundChangeToString( const BackgroundChange &bgc ) } /** - * @brief Write out the common tags for .SM files. + * @brief Turn a vector of lines into a single line joined by newline characters. + * @param lines the list of lines to join. + * @return the joined lines. */ +static RString JoinLineList( vector &lines ) +{ + for( unsigned i = 0; i < lines.size(); ++i ) + TrimRight( lines[i] ); + + // Skip leading blanks. + unsigned j = 0; + while( j < lines.size() && lines.size() == 0 ) + ++j; + + return join( "\r\n", lines.begin()+j, lines.end() ); +} + + +// A utility class to write timing tags more easily! +struct TimingTagWriter { + + vector *m_pvsLines; + RString m_sNext; + + TimingTagWriter( vector *pvsLines ): m_pvsLines (pvsLines) { } + + void Write( const int row, const char *value ) + { + m_pvsLines->push_back( m_sNext + ssprintf( "%.6f=%s", NoteRowToBeat(row), value ) ); + m_sNext = ","; + } + + void Write( const int row, const float value ) { Write( row, ssprintf( "%.6f", value ) ); } + void Write( const int row, const int value ) { Write( row, ssprintf( "%d", value ) ); } + void Write( const int row, const int a, const int b ) { Write( row, ssprintf( "%d=%d", a, b ) ); } + void Write( const int row, const float a, const float b ) { Write( row, ssprintf( "%.6f=%.6f", a, b) ); } + void Write( const int row, const float a, const float b, const unsigned short c ) + { Write( row, ssprintf( "%.6f=%.6f=%hd", a, b, c) ); } + + void Init( const RString sTag ) { m_sNext = "#" + sTag + ":"; } + void Finish( ) { m_pvsLines->push_back( ( m_sNext != "," ? m_sNext : "" ) + ";" ); } + +}; + +static void GetTimingTags( vector &lines, TimingData timing, bool bIsSong = false ) +{ + TimingTagWriter w ( &lines ); + + timing.TidyUpData(); + + w.Init( "BPMS" ); + FOREACH_CONST( BPMSegment, timing.m_BPMSegments, bs ) + w.Write( bs->m_iStartRow, bs->GetBPM() ); + w.Finish(); + + w.Init( "STOPS" ); + FOREACH_CONST( StopSegment, timing.m_StopSegments, ss ) + if( !ss->m_bDelay ) + w.Write( ss->m_iStartRow, ss->m_fStopSeconds ); + w.Finish(); + + w.Init( "DELAYS" ); + FOREACH_CONST( StopSegment, timing.m_StopSegments, ss ) + if( ss->m_bDelay ) + w.Write( ss->m_iStartRow, ss->m_fStopSeconds ); + w.Finish(); + + w.Init( "WARPS" ); + FOREACH_CONST( WarpSegment, timing.m_WarpSegments, ws ) + w.Write( ws->m_iStartRow, ws->m_fLengthBeats ); + w.Finish(); + + ASSERT( !timing.m_vTimeSignatureSegments.empty() ); + w.Init( "TIMESIGNATURES" ); + FOREACH_CONST( TimeSignatureSegment, timing.m_vTimeSignatureSegments, iter ) + w.Write( iter->m_iStartRow, iter->m_iNumerator, iter->m_iDenominator ); + w.Finish(); + + ASSERT( !timing.m_TickcountSegments.empty() ); + w.Init( "TICKCOUNTS" ); + FOREACH_CONST( TickcountSegment, timing.m_TickcountSegments, ts ) + w.Write( ts->m_iStartRow, ts->m_iTicks ); + w.Finish(); + + ASSERT( !timing.m_ComboSegments.empty() ); + w.Init( "COMBOS" ); + FOREACH_CONST( ComboSegment, timing.m_ComboSegments, cs ) + w.Write( cs->m_iStartRow, cs->m_iCombo ); + w.Finish(); + + // Song Timing should only have the initial value. + w.Init( "SPEEDS" ); + FOREACH_CONST( SpeedSegment, timing.m_SpeedSegments, ss ) + w.Write( ss->m_iStartRow, ss->m_fPercent, ss->m_fWait, ss->m_usMode ); + w.Finish(); + + if( !bIsSong ) + { + w.Init( "FAKES" ); + FOREACH_CONST( FakeSegment, timing.m_FakeSegments, fs ) + w.Write( fs->m_iStartRow, fs->m_fLengthBeats ); + w.Finish(); + } + + w.Init( "LABELS" ); + FOREACH_CONST( LabelSegment, timing.m_LabelSegments, ls ) + w.Write( ls->m_iStartRow, ls->m_sLabel.c_str() ); + w.Finish(); +} + +static void WriteTimingTags( RageFile &f, const TimingData &timing, bool bIsSong = false ) +{ + + vector lines; + + GetTimingTags( lines, timing, bIsSong ); + + f.PutLine( JoinLineList( lines ) ); + +} + +/** + * @brief Write out the common tags for .SSC files. * @param f the file in question. * @param out the Song in question. */ static void WriteGlobalTags( RageFile &f, const Song &out ) @@ -74,7 +195,7 @@ static void WriteGlobalTags( RageFile &f, const Song &out ) f.PutLine( "#INSTRUMENTTRACK:" + s + ";\n" ); } } - f.PutLine( ssprintf( "#OFFSET:%.6f;", out.m_Timing.m_fBeat0OffsetInSeconds ) ); + f.PutLine( ssprintf( "#OFFSET:%.6f;", out.m_SongTiming.m_fBeat0OffsetInSeconds ) ); f.PutLine( ssprintf( "#SAMPLESTART:%.6f;", out.m_fMusicSampleStartSeconds ) ); f.PutLine( ssprintf( "#SAMPLELENGTH:%.6f;", out.m_fMusicSampleLengthSeconds ) ); if( out.m_fSpecifiedLastBeat > 0 ) @@ -106,105 +227,7 @@ static void WriteGlobalTags( RageFile &f, const Song &out ) break; } - f.Write( "#BPMS:" ); - for( unsigned i=0; im_iStartRow), iter->m_iNumerator, iter->m_iDenominator ) ); - vector::const_iterator iter2 = iter; - iter2++; - if( iter2 != out.m_Timing.m_vTimeSignatureSegments.end() ) - f.Write( "," ); - } - f.PutLine( ";" ); - - ASSERT( !out.m_Timing.m_TickcountSegments.empty() ); - f.Write( "#TICKCOUNTS:" ); - for( unsigned i=0; i &lines ) -{ - for( unsigned i = 0; i < lines.size(); ++i ) - TrimRight( lines[i] ); - - // Skip leading blanks. - unsigned j = 0; - while( j < lines.size() && lines.size() == 0 ) - ++j; - - return join( "\r\n", lines.begin()+j, lines.end() ); -} - /** * @brief Retrieve the individual batches of NoteData. * @param song the Song in question. @@ -307,19 +313,12 @@ static RString GetSSCNoteData( const Song &song, const Steps &in, bool bSavingCa lines.push_back( ssprintf( "#CREDIT:%s;", SmEscape(in.GetCredit()).c_str() ) ); - /* - * TODO: Remove this block, transplant above code - * below for Split Timing. -Wolfman2000 */ - lines.push_back( "#BPMS:;" ); - lines.push_back( "#STOPS:;" ); - lines.push_back( "#DELAYS:;" ); - lines.push_back( "#WARPS:;" ); - lines.push_back( "#LABELS:;" ); - lines.push_back( "#TIMESIGNATURES:;" ); - lines.push_back( "#TICKCOUNTS:;" ); + GetTimingTags( lines, in.m_Timing ); + + // For now, attacks are NOT in use for the step. lines.push_back( "#ATTACKS:;" ); - lines.push_back( "#COMBOS:;" ); - + lines.push_back( ssprintf( "#OFFSET:%.6f;", in.m_Timing.m_fBeat0OffsetInSeconds ) ); + RString sNoteData; in.GetSMNoteData( sNoteData ); @@ -350,6 +349,7 @@ bool NotesWriterSSC::Write( RString sPath, const Song &out, const vector } WriteGlobalTags( f, out ); + if( bSavingCache ) { f.PutLine( ssprintf( "// cache tags:" ) ); diff --git a/src/Player.cpp b/src/Player.cpp index 570112bb0e..98483728b7 100644 --- a/src/Player.cpp +++ b/src/Player.cpp @@ -506,7 +506,7 @@ void Player::Load() m_LastTapNoteScore = TNS_None; // The editor can start playing in the middle of the song. - const int iNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat ); + const int iNoteRow = BeatToNoteRowNotRounded( m_pPlayerState->m_Position.m_fSongBeat ); m_iFirstUncrossedRow = iNoteRow - 1; m_pJudgedRows->Reset( iNoteRow ); @@ -541,6 +541,8 @@ void Player::Load() if( GAMESTATE->m_pCurGame->m_bAllowHopos ) NoteDataUtil::SetHopoPossibleFlags( pSong, m_NoteData ); + m_Timing = &GAMESTATE->m_pCurSteps[pn]->m_Timing; + switch( GAMESTATE->m_PlayMode ) { case PLAY_MODE_RAVE: @@ -693,7 +695,7 @@ void Player::Update( float fDeltaTime ) return; } - const float fSongBeat = GAMESTATE->m_fSongBeat; + const float fSongBeat = m_pPlayerState->m_Position.m_fSongBeat; const int iSongRow = BeatToNoteRow( fSongBeat ); // Optimization: Don't spend time processing the things below that won't show @@ -795,7 +797,7 @@ void Player::Update( float fDeltaTime ) // Check for a strum miss if( m_pPlayerState->m_fLastStrumMusicSeconds != -1 && - m_pPlayerState->m_fLastStrumMusicSeconds + g_fTimingWindowStrum < GAMESTATE->m_fMusicSeconds ) + m_pPlayerState->m_fLastStrumMusicSeconds + g_fTimingWindowStrum < m_pPlayerState->m_Position.m_fMusicSeconds ) { DoStrumMiss(); } @@ -917,12 +919,12 @@ void Player::Update( float fDeltaTime ) /* We want to send the crossed row message exactly when we cross the row--not * .5 before the row. Use a very slow song (around 2 BPM) as a test case: without * rounding, autoplay steps early. -glenn */ - const int iRowNow = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat ); + const int iRowNow = BeatToNoteRowNotRounded( m_pPlayerState->m_Position.m_fSongBeat ); if( iRowNow >= 0 ) { if( GAMESTATE->IsPlayerEnabled(m_pPlayerState) ) { - if(GAMESTATE->m_bDelay) + if(m_pPlayerState->m_Position.m_bDelay) { if( !m_bDelay ) m_bDelay = true; @@ -1528,7 +1530,7 @@ int Player::GetClosestNoteDirectional( int col, int iStartRow, int iEndRow, bool // Is this the row we want? do { const TapNote &tn = begin->second; - if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( begin->first ) ) + if( m_Timing->IsWarpAtRow( begin->first ) || m_Timing->IsFakeAtRow( begin->first ) ) break; if( tn.type == TapNote::empty ) break; @@ -1579,7 +1581,7 @@ int Player::GetClosestNonEmptyRowDirectional( int iStartRow, int iEndRow, bool b ++iter; continue; } - if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( iter.Row() ) ) + if( m_Timing->IsWarpAtRow( iter.Row() ) || m_Timing->IsFakeAtRow( iter.Row() ) ) { ++iter; continue; @@ -1650,7 +1652,7 @@ void Player::Fret( int col, int row, const RageTimer &tm, bool bHeld, bool bRele } // Handle hammer-ons and pull-offs - const float fPositionSeconds = GAMESTATE->m_fMusicSeconds - tm.Ago(); + const float fPositionSeconds = m_pPlayerState->m_Position.m_fMusicSeconds - tm.Ago(); int iHopoCol = -1; bool bDoHopo = m_pPlayerState->m_fLastHopoNoteMusicSeconds != -1 && @@ -1703,7 +1705,7 @@ void Player::Fret( int col, int row, const RageTimer &tm, bool bHeld, bool bRele // Check if this fret breaks all active holds. if( !bRelease ) { - const float fSongBeat = GAMESTATE->m_fSongBeat; + const float fSongBeat = m_pPlayerState->m_Position.m_fSongBeat; const int iSongRow = BeatToNoteRow( fSongBeat ); int iMaxHoldCol = -1; @@ -1745,7 +1747,7 @@ void Player::Strum( int col, int row, const RageTimer &tm, bool bHeld, bool bRel DoStrumMiss(); } - m_pPlayerState->m_fLastStrumMusicSeconds = GAMESTATE->m_fMusicSeconds; + m_pPlayerState->m_fLastStrumMusicSeconds = m_pPlayerState->m_Position.m_fMusicSeconds; StepStrumHopo( col, row, tm, bHeld, bRelease, ButtonType_StrumFretsChanged ); } @@ -1794,7 +1796,7 @@ void Player::ScoreAllActiveHoldsLetGo() { if( PENALIZE_TAP_SCORE_NONE ) { - const float fSongBeat = GAMESTATE->m_fSongBeat; + const float fSongBeat = m_pPlayerState->m_Position.m_fSongBeat; const int iSongRow = BeatToNoteRow( fSongBeat ); // Score all active holds to NotHeld @@ -1857,8 +1859,8 @@ void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, b // Do everything that depends on a RageTimer here; // set your breakpoints somewhere after this block. - const float fLastBeatUpdate = GAMESTATE->m_LastBeatUpdate.Ago(); - const float fPositionSeconds = GAMESTATE->m_fMusicSeconds - tm.Ago(); + const float fLastBeatUpdate = m_pPlayerState->m_Position.m_LastBeatUpdate.Ago(); + const float fPositionSeconds = m_pPlayerState->m_Position.m_fMusicSeconds - tm.Ago(); const float fTimeSinceStep = tm.Ago(); switch( pbt ) @@ -1873,7 +1875,16 @@ void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, b break; } - const float fSongBeat = GAMESTATE->m_pCurSong ? GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ) : GAMESTATE->m_fSongBeat; + float fSongBeat = m_pPlayerState->m_Position.m_fSongBeat; + + if( GAMESTATE->m_pCurSong ) + { + fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds ); + + if( GAMESTATE->m_pCurSteps[m_pPlayerState->m_PlayerNumber] ) + fSongBeat = m_Timing->GetBeatFromElapsedTime( fPositionSeconds ); + } + const int iSongRow = row == -1 ? BeatToNoteRow( fSongBeat ) : row; if( col != -1 && !bRelease ) @@ -2011,8 +2022,8 @@ void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, b * "jack hammers." Hmm. */ const int iStepSearchRows = max( - BeatToNoteRow( GAMESTATE->m_pCurSong->m_Timing.GetBeatFromElapsedTime( GAMESTATE->m_fMusicSeconds + StepSearchDistance ) ) - iSongRow, - iSongRow - BeatToNoteRow( GAMESTATE->m_pCurSong->m_Timing.GetBeatFromElapsedTime( GAMESTATE->m_fMusicSeconds - StepSearchDistance ) ) + BeatToNoteRow( m_Timing->GetBeatFromElapsedTime( m_pPlayerState->m_Position.m_fMusicSeconds + StepSearchDistance ) ) - iSongRow, + iSongRow - BeatToNoteRow( m_Timing->GetBeatFromElapsedTime( m_pPlayerState->m_Position.m_fMusicSeconds - StepSearchDistance ) ) ) + ROWS_PER_BEAT; int iRowOfOverlappingNoteOrRow = row; if( row == -1 ) @@ -2039,7 +2050,7 @@ void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, b float fNoteOffset = 0.0f; // we need this later if we are autosyncing const float fStepBeat = NoteRowToBeat( iRowOfOverlappingNoteOrRow ); - const float fStepSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fStepBeat); + const float fStepSeconds = m_Timing->GetElapsedTimeFromBeat(fStepBeat); if( row == -1 ) { @@ -2048,7 +2059,7 @@ void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, b /* GAMESTATE->m_fMusicSeconds is the music time as of GAMESTATE->m_LastBeatUpdate. Figure * out what the music time is as of now. */ - const float fCurrentMusicSeconds = GAMESTATE->m_fMusicSeconds + (fLastBeatUpdate*GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate); + const float fCurrentMusicSeconds = m_pPlayerState->m_Position.m_fMusicSeconds + (fLastBeatUpdate*GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate); // ... which means it happened at this point in the music: const float fMusicSeconds = fCurrentMusicSeconds - fTimeSinceStep * GAMESTATE->m_SongOptions.GetCurrent().m_fMusicRate; @@ -2089,7 +2100,7 @@ void Player::StepStrumHopo( int col, int row, const RageTimer &tm, bool bHeld, b // Stepped too close to mine? if( !bRelease && ( REQUIRE_STEP_ON_MINES == !bHeld ) && fSecondsFromExact <= GetWindowSeconds(TW_Mine) && - !GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow(iSongRow) ) + !m_Timing->IsWarpAtRow(iSongRow) && !m_Timing->IsFakeAtRow(iSongRow)) score = TNS_HitMine; break; @@ -2557,14 +2568,14 @@ void Player::UpdateTapNotesMissedOlderThan( float fMissIfOlderThanSeconds ) { //LOG->Trace( "Steps::UpdateTapNotesMissedOlderThan(%f)", fMissIfOlderThanThisBeat ); int iMissIfOlderThanThisRow; - const float fEarliestTime = GAMESTATE->m_fMusicSeconds - fMissIfOlderThanSeconds; + const float fEarliestTime = m_pPlayerState->m_Position.m_fMusicSeconds - fMissIfOlderThanSeconds; { bool bFreeze, bDelay; float fMissIfOlderThanThisBeat; float fThrowAway; int iWarpBeginRow; float fWarpLength; - GAMESTATE->m_pCurSong->m_Timing.GetBeatAndBPSFromElapsedTime( fEarliestTime, fMissIfOlderThanThisBeat, fThrowAway, bFreeze, bDelay, iWarpBeginRow, fWarpLength ); + m_Timing->GetBeatAndBPSFromElapsedTime( fEarliestTime, fMissIfOlderThanThisBeat, fThrowAway, bFreeze, bDelay, iWarpBeginRow, fWarpLength ); iMissIfOlderThanThisRow = BeatToNoteRow( fMissIfOlderThanThisBeat ); if( bFreeze || bDelay ) @@ -2586,8 +2597,8 @@ void Player::UpdateTapNotesMissedOlderThan( float fMissIfOlderThanSeconds ) if( !NeedsTapJudging(tn) ) continue; - // Ignore all notes that are skipped via WARPS. - if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( iter.Row() ) ) + // Ignore all notes in WarpSegments or FakeSegments. + if( m_Timing->IsWarpAtRow( iter.Row() ) || m_Timing->IsFakeAtRow( iter.Row() ) ) continue; if( tn.type == TapNote::mine ) @@ -2610,7 +2621,7 @@ void Player::UpdateTapNotesMissedOlderThan( float fMissIfOlderThanSeconds ) void Player::UpdateJudgedRows() { - const int iEndRow = BeatToNoteRow( GAMESTATE->m_fSongBeat ); + const int iEndRow = BeatToNoteRow( m_pPlayerState->m_Position.m_fSongBeat ); bool bAllJudged = true; const bool bSeparately = GAMESTATE->GetCurrentGame()->m_bCountNotesSeparately; @@ -2621,8 +2632,8 @@ void Player::UpdateJudgedRows() { int iRow = iter.Row(); - // If row is within a warp section, ignore it. -aj - if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow(iRow) ) + // Do not judge arrows in WarpSegments or FakeSegments + if( m_Timing->IsWarpAtRow(iRow) || m_Timing->IsFakeAtRow(iRow) ) continue; if( iLastSeenRow != iRow ) @@ -2843,13 +2854,13 @@ void Player::CrossedRows( int iLastRowCrossed, const RageTimer &now ) int iCheckpointFrequencyRows = ROWS_PER_BEAT/2; if( CHECKPOINTS_USE_TICKCOUNTS ) { - int tickCurrent = GAMESTATE->m_pCurSong->m_Timing.GetTickcountAtRow( iLastRowCrossed ); + int tickCurrent = m_Timing->GetTickcountAtRow( iLastRowCrossed ); // There are some charts that don't want tickcounts involved at all. iCheckpointFrequencyRows = (tickCurrent > 0 ? ROWS_PER_BEAT / tickCurrent : 0); } else if( CHECKPOINTS_USE_TIME_SIGNATURES ) { - TimeSignatureSegment tSignature = GAMESTATE->m_pCurSong->m_Timing.GetTimeSignatureSegmentAtBeat( NoteRowToBeat( iLastRowCrossed ) ); + TimeSignatureSegment tSignature = m_Timing->GetTimeSignatureSegmentAtBeat( NoteRowToBeat( iLastRowCrossed ) ); // Most songs are in 4/4 time. The frequency for checking tick counts should reflect that. iCheckpointFrequencyRows = ROWS_PER_BEAT * tSignature.m_iDenominator / (tSignature.m_iNumerator * 4); @@ -2956,8 +2967,8 @@ void Player::HandleTapRowScore( unsigned row ) bNoCheating = false; #endif - // Warp hackery. -aj - if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( row ) ) + // Do not score rows in WarpSegments or FakeSegments + if( m_Timing->IsWarpAtRow( row ) || m_Timing->IsFakeAtRow( row ) ) return; if( GAMESTATE->m_bDemonstrationOrJukebox ) @@ -3060,8 +3071,8 @@ void Player::HandleHoldCheckpoint( int iRow, int iNumHoldsHeldThisRow, int iNumH bNoCheating = false; #endif - // More warp hackery. -aj - if( GAMESTATE->m_pCurSong->m_Timing.IsWarpAtRow( iRow ) ) + // WarpSegments and FakeSegments aren't judged in any way. + if( m_Timing->IsWarpAtRow( iRow ) || m_Timing->IsFakeAtRow( iRow ) ) return; // don't accumulate combo if AutoPlay is on. @@ -3257,7 +3268,7 @@ void Player::SetCombo( int iCombo, int iMisses ) } else { - bPastBeginning = GAMESTATE->m_fMusicSeconds > GAMESTATE->m_pCurSong->m_fMusicLengthSeconds * PERCENT_UNTIL_COLOR_COMBO; + bPastBeginning = m_pPlayerState->m_Position.m_fMusicSeconds > GAMESTATE->m_pCurSong->m_fMusicLengthSeconds * PERCENT_UNTIL_COLOR_COMBO; } if( m_bSendJudgmentAndComboMessages ) diff --git a/src/Player.h b/src/Player.h index b8923b9372..2d699e4913 100644 --- a/src/Player.h +++ b/src/Player.h @@ -10,6 +10,7 @@ #include "ScreenMessage.h" #include "ThemeMetric.h" #include "InputEventPlus.h" +#include "TimingData.h" class ScoreDisplay; class LifeMeter; @@ -147,6 +148,7 @@ protected: PlayerState *m_pPlayerState; /** @brief The player's present stage stats. */ PlayerStageStats *m_pPlayerStageStats; + TimingData *m_Timing; float m_fNoteFieldHeight; bool m_bPaused; diff --git a/src/PlayerOptions.cpp b/src/PlayerOptions.cpp index 036f1d314c..cbfb031bbf 100644 --- a/src/PlayerOptions.cpp +++ b/src/PlayerOptions.cpp @@ -680,7 +680,7 @@ bool PlayerOptions::operator==( const PlayerOptions &other ) const bool PlayerOptions::IsEasierForSongAndSteps( Song* pSong, Steps* pSteps, PlayerNumber pn ) const { - if( m_fTimeSpacing && pSong->HasSignificantBpmChangesOrStops() ) + if( m_fTimeSpacing && pSteps->HasSignificantTimingChanges() ) return true; const RadarValues &rv = pSteps->GetRadarValues( pn ); if( m_bTransforms[TRANSFORM_NOHOLDS] && rv[RadarCategory_Holds]>0 ) diff --git a/src/PlayerOptions.h b/src/PlayerOptions.h index 8a66d1d758..c6ea986160 100644 --- a/src/PlayerOptions.h +++ b/src/PlayerOptions.h @@ -35,8 +35,9 @@ public: m_fPassmark(0), m_SpeedfPassmark(1.0f), m_fRandomSpeed(0), m_SpeedfRandomSpeed(1.0f), m_bMuteOnError(false), m_FailType(FAIL_IMMEDIATE), - m_ScoreDisplay(SCORING_ADD), m_sNoteSkin("") + m_ScoreDisplay(SCORING_ADD) { + m_sNoteSkin = ""; ZERO( m_fAccels ); ONE( m_SpeedfAccels ); ZERO( m_fEffects ); ONE( m_SpeedfEffects ); ZERO( m_fAppearances ); ONE( m_SpeedfAppearances ); diff --git a/src/PlayerState.cpp b/src/PlayerState.cpp index 070e96c7be..9d7b652851 100644 --- a/src/PlayerState.cpp +++ b/src/PlayerState.cpp @@ -69,8 +69,8 @@ void PlayerState::Update( float fDelta ) bool bCurrentlyEnabled = attack.bGlobal || - ( attack.fStartSecond < GAMESTATE->m_fMusicSeconds && - GAMESTATE->m_fMusicSeconds < attack.fStartSecond+attack.fSecsRemaining ); + ( attack.fStartSecond < m_Position.m_fMusicSeconds && + m_Position.m_fMusicSeconds < attack.fStartSecond+attack.fSecsRemaining ); if( m_ActiveAttacks[s].bOn == bCurrentlyEnabled ) continue; // OK @@ -116,7 +116,7 @@ void PlayerState::LaunchAttack( const Attack& a ) * so Player::Update knows to apply attack transforms correctly. (yuck) */ m_ModsToApply.push_back( attack ); if( attack.fStartSecond == -1 ) - attack.fStartSecond = GAMESTATE->m_fMusicSeconds; + attack.fStartSecond = m_Position.m_fMusicSeconds; m_ActiveAttacks.push_back( attack ); RebuildPlayerOptionsFromActiveAttacks(); @@ -197,6 +197,11 @@ class LunaPlayerState: public Luna { public: DEFINE_METHOD( GetPlayerNumber, m_PlayerNumber ); + static int GetSongPosition( T* p, lua_State *L ) + { + p->m_Position.PushSelf(L); + return 1; + } DEFINE_METHOD( GetMultiPlayerNumber, m_mp ); DEFINE_METHOD( GetPlayerController, m_PlayerController ); static int SetPlayerOptions( T* p, lua_State *L ) @@ -246,6 +251,7 @@ public: ADD_METHOD( GetPlayerOptionsArray ); ADD_METHOD( GetPlayerOptionsString ); ADD_METHOD( GetCurrentPlayerOptions ); + ADD_METHOD( GetSongPosition ); ADD_METHOD( GetHealthState ); } }; diff --git a/src/PlayerState.h b/src/PlayerState.h index 6df9dc662d..59239ca915 100644 --- a/src/PlayerState.h +++ b/src/PlayerState.h @@ -3,6 +3,7 @@ #ifndef PlayerState_H #define PlayerState_H +#include "SongPosition.h" #include "Attack.h" #include "ModsGroup.h" #include "PlayerNumber.h" @@ -38,6 +39,9 @@ public: */ MultiPlayer m_mp; + // Music statistics: + SongPosition m_Position; + /** * @brief Change the PlayerOptions to their default. * @param l the level of mods to reset. diff --git a/src/ScoreKeeperNormal.cpp b/src/ScoreKeeperNormal.cpp index 3a024ec348..c1422942f9 100644 --- a/src/ScoreKeeperNormal.cpp +++ b/src/ScoreKeeperNormal.cpp @@ -443,7 +443,7 @@ void ScoreKeeperNormal::HandleComboInternal( int iNumHitContinueCombo, int iNumH if( iNumBreakCombo == 0 ) { - TimingData td = GAMESTATE->m_pCurSong->m_Timing; + TimingData td = GAMESTATE->m_pCurSteps[m_pPlayerState->m_PlayerNumber]->m_Timing; int multiplier = ( iRow == -1 ? 1 : td.GetComboSegmentAtRow( iRow ).m_iCombo ); m_pPlayerStageStats->m_iCurCombo += iNumHitContinueCombo * multiplier; } @@ -463,7 +463,7 @@ void ScoreKeeperNormal::HandleRowComboInternal( TapNoteScore tns, int iNumTapsIn if ( tns >= m_MinScoreToContinueCombo ) { m_pPlayerStageStats->m_iCurMissCombo = 0; - TimingData td = GAMESTATE->m_pCurSong->m_Timing; + TimingData td = GAMESTATE->m_pCurSteps[m_pPlayerState->m_PlayerNumber]->m_Timing; int multiplier = ( iRow == -1 ? 1 : td.GetComboSegmentAtRow( iRow ).m_iCombo ); m_pPlayerStageStats->m_iCurCombo += iNumTapsInRow * multiplier; } diff --git a/src/ScreenEdit.cpp b/src/ScreenEdit.cpp index 99e5f0b69e..41ce749a0b 100644 --- a/src/ScreenEdit.cpp +++ b/src/ScreenEdit.cpp @@ -77,12 +77,16 @@ AutoScreenMessage( SM_BackFromDifficultyMeterChange ); AutoScreenMessage( SM_BackFromBPMChange ); AutoScreenMessage( SM_BackFromStopChange ); AutoScreenMessage( SM_BackFromDelayChange ); -AutoScreenMessage( SM_BackFromTimeSignatureNumeratorChange ); -AutoScreenMessage( SM_BackFromTimeSignatureDenominatorChange ); +AutoScreenMessage( SM_BackFromTimeSignatureChange ); AutoScreenMessage( SM_BackFromTickcountChange ); AutoScreenMessage( SM_BackFromComboChange ); AutoScreenMessage( SM_BackFromLabelChange ); AutoScreenMessage( SM_BackFromWarpChange ); +AutoScreenMessage( SM_BackFromSpeedPercentChange ); +AutoScreenMessage( SM_BackFromSpeedWaitChange ); +AutoScreenMessage( SM_BackFromSpeedModeChange ); +AutoScreenMessage( SM_BackFromFakeChange ); +AutoScreenMessage( SM_DoEraseStepTiming ); AutoScreenMessage( SM_DoSaveAndExit ); AutoScreenMessage( SM_DoExit ); AutoScreenMessage( SM_SaveSuccessful ); @@ -194,6 +198,9 @@ void ScreenEdit::InitEditMappings() return; case EditMode_Full: + // F4 = Show timing menu + m_EditMappingsDeviceInput.button[EDIT_BUTTON_OPEN_TIMING_MENU][0] = DeviceInput(DEVICE_KEYBOARD, KEY_F4); + /* Don't allow F5/F6 in home mode. It breaks the "delay creation until first save" logic. */ m_EditMappingsDeviceInput.button[EDIT_BUTTON_OPEN_PREV_STEPS][0] = DeviceInput(DEVICE_KEYBOARD, KEY_F5); m_EditMappingsDeviceInput.button[EDIT_BUTTON_OPEN_NEXT_STEPS][0] = DeviceInput(DEVICE_KEYBOARD, KEY_F6); @@ -307,6 +314,9 @@ void ScreenEdit::InitEditMappings() // Switch players, if it makes sense to do so. m_EditMappingsDeviceInput.button[EDIT_BUTTON_SWITCH_PLAYERS][0] = DeviceInput(DEVICE_KEYBOARD, KEY_SLASH); + + // Allow song and step timing to be swapped. + m_EditMappingsDeviceInput.button[EDIT_BUTTON_SWITCH_TIMINGS][0] = DeviceInput(DEVICE_KEYBOARD, KEY_Ct); m_PlayMappingsDeviceInput.button[EDIT_BUTTON_RETURN_TO_EDIT][0] = DeviceInput(DEVICE_KEYBOARD, KEY_ESC); m_PlayMappingsMenuButton.button[EDIT_BUTTON_RETURN_TO_EDIT][1] = GAME_BUTTON_BACK; @@ -548,7 +558,6 @@ static MenuDef g_SongInformation( MenuRowDef( ScreenEdit::main_title_transliteration, "Main title transliteration", true, EditMode_Practice, true, true, 0, NULL ), MenuRowDef( ScreenEdit::sub_title_transliteration, "Sub title transliteration", true, EditMode_Practice, true, true, 0, NULL ), MenuRowDef( ScreenEdit::artist_transliteration, "Artist transliteration", true, EditMode_Practice, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::beat_0_offset, "Beat 0 Offset", true, EditMode_Full, true, true, 0, NULL ), MenuRowDef( ScreenEdit::last_beat_hint, "Last beat hint", true, EditMode_Full, true, true, 0, NULL ), MenuRowDef( ScreenEdit::preview_start, "Preview Start", true, EditMode_Full, true, true, 0, NULL ), MenuRowDef( ScreenEdit::preview_length, "Preview Length", true, EditMode_Full, true, true, 0, NULL ), @@ -557,17 +566,23 @@ static MenuDef g_SongInformation( MenuRowDef( ScreenEdit::max_bpm, "Max BPM", true, EditMode_Full, true, true, 0, NULL ) ); + static MenuDef g_TimingDataInformation( "ScreenMiniMenuTimingDataInformation", - MenuRowDef( ScreenEdit::bpm, "Edit BPM change", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::stop, "Edit stop", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::delay, "Edit delay", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::time_signature_numerator, "Edit time signature (top)", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::time_signature_denominator, "Edit time signature (bottom)", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::tickcount, "Edit tickcount", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::combo, "Edit combo", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::label, "Edit label", true, EditMode_Full, true, true, 0, NULL ), - MenuRowDef( ScreenEdit::warp, "Edit warp", true, EditMode_Full, true, true, 0, NULL ) + MenuRowDef( ScreenEdit::beat_0_offset, "Beat 0 Offset", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::bpm, "Edit BPM change", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::stop, "Edit stop", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::delay, "Edit delay", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::time_signature, "Edit time signature", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::label, "Edit label", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::tickcount, "Edit tickcount", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::combo, "Edit combo", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::warp, "Edit warp", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::speed_percent, "Edit speed (percent)", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::speed_wait, "Edit speed (wait)", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::speed_mode, "Edit speed (mode)", true, EditMode_Full, true, true, 0, "Beats", "Seconds" ), + MenuRowDef( ScreenEdit::fake, "Edit fake", true, EditMode_Full, true, true, 0, NULL ), + MenuRowDef( ScreenEdit::erase_step_timing, "Erase step timing", true, EditMode_Full, true, true, 0, NULL ) ); enum { song_bganimation, song_movie, song_bitmap, global_bganimation, global_movie, global_movie_song_group, global_movie_song_group_and_genre, dynamic_random, baked_random, none }; @@ -689,6 +704,9 @@ REGISTER_SCREEN_CLASS( ScreenEdit ); void ScreenEdit::Init() { m_pSoundMusic = NULL; + + GAMESTATE->m_bIsUsingStepTiming = true; + GAMESTATE->m_bInStepEditor = true; SubscribeToMessage( "Judgment" ); @@ -722,8 +740,8 @@ void ScreenEdit::Init() GAMESTATE->m_bGameplayLeadIn.Set( true ); GAMESTATE->m_EditMode = EDIT_MODE.GetValue(); - GAMESTATE->m_fSongBeat = 0; - m_fTrailingBeat = GAMESTATE->m_fSongBeat; + SetBeat(0); + m_fTrailingBeat = 0; m_iShiftAnchor = -1; m_iStartPlayingAt = -1; @@ -824,6 +842,8 @@ void ScreenEdit::Init() m_soundMarker.Load( THEME->GetPathS("ScreenEdit","marker"), true ); m_soundValueIncrease.Load( THEME->GetPathS("ScreenEdit","value increase"), true ); m_soundValueDecrease.Load( THEME->GetPathS("ScreenEdit","value decrease"), true ); + m_soundSwitchPlayer.Load( THEME->GetPathS("ScreenEdit","switch player"), true ); + m_soundSwitchTiming.Load( THEME->GetPathS("ScreenEdit","switch timing"), true ); m_soundSwitchSteps.Load( THEME->GetPathS("ScreenEdit","switch steps") ); m_soundSave.Load( THEME->GetPathS("ScreenEdit","save") ); m_GameplayAssist.Init(); @@ -844,6 +864,11 @@ ScreenEdit::~ScreenEdit() LOG->Trace( "ScreenEdit::~ScreenEdit()" ); m_pSoundMusic->StopPlaying(); + + // Go back to Step Timing on leave. + GAMESTATE->m_bIsUsingStepTiming = true; + // DEFINITELY reset the InStepEditor variable. + GAMESTATE->m_bInStepEditor = false; } void ScreenEdit::BeginScreen() @@ -913,11 +938,13 @@ void ScreenEdit::Update( float fDeltaTime ) { RageTimer tm; const float fSeconds = m_pSoundMusic->GetPositionSeconds( NULL, &tm ); - GAMESTATE->UpdateSongPosition( fSeconds, GAMESTATE->m_pCurSong->m_Timing, tm ); + GAMESTATE->UpdateSongPosition( fSeconds, GAMESTATE->m_pCurSong->m_SongTiming, tm, true ); } if( m_EditState == STATE_RECORDING ) { + // TODO: Find a way to prevent STATE_RECORDING when in Song Timing. + for( int t=0; tGetCurrentStyle()->m_iColsPerPlayer; t++ ) // for each track { GameInput GameI = GAMESTATE->GetCurrentStyle()->StyleInputToGameInput( t, PLAYER_1 ); @@ -927,12 +954,12 @@ void ScreenEdit::Update( float fDeltaTime ) continue; float fStartPlayingAtBeat = NoteRowToBeat(m_iStartPlayingAt); - if( GAMESTATE->m_fSongBeat <= fStartPlayingAtBeat ) + if( GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat <= fStartPlayingAtBeat ) continue; float fStartedHoldingSeconds = m_pSoundMusic->GetPositionSeconds() - fSecsHeld; - float fStartBeat = max( fStartPlayingAtBeat, m_pSong->GetBeatFromElapsedTime(fStartedHoldingSeconds) ); - float fEndBeat = max( fStartBeat, GAMESTATE->m_fSongBeat ); + float fStartBeat = max( fStartPlayingAtBeat, m_pSteps->m_Timing.GetBeatFromElapsedTime(fStartedHoldingSeconds) ); + float fEndBeat = max( fStartBeat, GetBeat() ); fEndBeat = min( fEndBeat, NoteRowToBeat(m_iStopPlayingAt) ); // Round start and end to the nearest snap interval @@ -976,12 +1003,13 @@ void ScreenEdit::Update( float fDeltaTime ) float fLastBeat = NoteRowToBeat(m_iStopPlayingAt); if( bButtonIsBeingPressed && m_EditState == STATE_RECORDING ) { - float fSeconds = m_pSong->m_Timing.GetElapsedTimeFromBeat( fLastBeat ); - fLastBeat = m_pSong->m_Timing.GetBeatFromElapsedTime( fSeconds + 0.5f ); + float fSeconds = m_pSteps->m_Timing.GetElapsedTimeFromBeat( fLastBeat ); + fLastBeat = m_pSteps->m_Timing.GetBeatFromElapsedTime( fSeconds + 0.5f ); } - float fStopAtSeconds = m_pSong->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(m_iStopPlayingAt) ) + 1; - if( GAMESTATE->m_fMusicSeconds > fStopAtSeconds ) + // This should be fine: it will convert to the same I believe. -Wolfman2000 + float fStopAtSeconds = m_pSteps->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(m_iStopPlayingAt) ) + 1; + if( GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fMusicSeconds > fStopAtSeconds ) { // loop TransitionEditState( STATE_PLAYING ); @@ -994,12 +1022,12 @@ void ScreenEdit::Update( float fDeltaTime ) // Update trailing beat - float fDelta = GAMESTATE->m_fSongBeat - m_fTrailingBeat; + float fDelta = GetBeat() - m_fTrailingBeat; if( fabsf(fDelta) < 10 ) - fapproach( m_fTrailingBeat, GAMESTATE->m_fSongBeat, + fapproach( m_fTrailingBeat, GetBeat(), fDeltaTime*40 / m_NoteFieldEdit.GetPlayerState()->m_PlayerOptions.GetCurrent().m_fScrollSpeed ); else - fapproach( m_fTrailingBeat, GAMESTATE->m_fSongBeat, + fapproach( m_fTrailingBeat, GetBeat(), fabsf(fDelta) * fDeltaTime*5 ); PlayTicks(); @@ -1025,6 +1053,9 @@ static LocalizedString MINES("ScreenEdit", "Mines"); static LocalizedString ROLLS("ScreenEdit", "Rolls"); static LocalizedString LIFTS("ScreenEdit", "Lifts"); static LocalizedString FAKES("ScreenEdit", "Fakes"); +static LocalizedString TIMING_MODE("ScreenEdit", "Timing Mode"); +static LocalizedString STEP_TIMING("ScreenEdit", "Step Timing"); +static LocalizedString SONG_TIMING("ScreenEdit", "Song Timing"); static LocalizedString BEAT_0_OFFSET("ScreenEdit", "Beat 0 offset"); static LocalizedString PREVIEW_START("ScreenEdit", "Preview Start"); static LocalizedString PREVIEW_LENGTH("ScreenEdit", "Preview Length"); @@ -1050,6 +1081,7 @@ static ThemeMetric NUM_HANDS_FORMAT("ScreenEdit", "NumHandsFormat"); static ThemeMetric NUM_ROLLS_FORMAT("ScreenEdit", "NumRollsFormat"); static ThemeMetric NUM_LIFTS_FORMAT("ScreenEdit", "NumLiftsFormat"); static ThemeMetric NUM_FAKES_FORMAT("ScreenEdit", "NumFakesFormat"); +static ThemeMetric TIMING_MODE_FORMAT("ScreenEdit", "TimingModeFormat"); static ThemeMetric BEAT_0_OFFSET_FORMAT("ScreenEdit", "Beat0OffsetFormat"); static ThemeMetric PREVIEW_START_FORMAT("ScreenEdit", "PreviewStartFormat"); static ThemeMetric PREVIEW_LENGTH_FORMAT("ScreenEdit", "PreviewLengthFormat"); @@ -1070,8 +1102,8 @@ void ScreenEdit::UpdateTextInfo() RString sNoteType = ssprintf( NOTES.GetValue(), NoteTypeToLocalizedString(m_SnapDisplay.GetNoteType()).c_str() ); RString sText; - sText += ssprintf( CURRENT_BEAT_FORMAT.GetValue(), CURRENT_BEAT.GetValue().c_str(), GAMESTATE->m_fSongBeat ); - sText += ssprintf( CURRENT_SECOND_FORMAT.GetValue(), CURRENT_SECOND.GetValue().c_str(), m_pSong->GetElapsedTimeFromBeat(GAMESTATE->m_fSongBeat) ); + sText += ssprintf( CURRENT_BEAT_FORMAT.GetValue(), CURRENT_BEAT.GetValue().c_str(), GetBeat() ); + sText += ssprintf( CURRENT_SECOND_FORMAT.GetValue(), CURRENT_SECOND.GetValue().c_str(), GetAppropriateTiming().GetElapsedTimeFromBeat(GetBeat()) ); switch( EDIT_MODE.GetValue() ) { DEFAULT_FAIL( EDIT_MODE.GetValue() ); @@ -1129,7 +1161,14 @@ void ScreenEdit::UpdateTextInfo() case EditMode_Home: break; case EditMode_Full: - sText += ssprintf( BEAT_0_OFFSET_FORMAT.GetValue(), BEAT_0_OFFSET.GetValue().c_str(), m_pSong->m_Timing.m_fBeat0OffsetInSeconds ); + sText += ssprintf( TIMING_MODE_FORMAT.GetValue(), + TIMING_MODE.GetValue().c_str(), + ( GAMESTATE->m_bIsUsingStepTiming ? + STEP_TIMING.GetValue().c_str() : + SONG_TIMING.GetValue().c_str() ) ); + sText += ssprintf( BEAT_0_OFFSET_FORMAT.GetValue(), + BEAT_0_OFFSET.GetValue().c_str(), + GetAppropriateTiming().m_fBeat0OffsetInSeconds ); sText += ssprintf( PREVIEW_START_FORMAT.GetValue(), PREVIEW_START.GetValue().c_str(), m_pSong->m_fMusicSampleStartSeconds ); sText += ssprintf( PREVIEW_LENGTH_FORMAT.GetValue(), PREVIEW_LENGTH.GetValue().c_str(), m_pSong->m_fMusicSampleLengthSeconds ); break; @@ -1140,26 +1179,42 @@ void ScreenEdit::UpdateTextInfo() void ScreenEdit::DrawPrimitives() { - // HACK: Draw using the trailing beat - float fSongBeat = GAMESTATE->m_fSongBeat; // save song beat - float fSongBeatNoOffset = GAMESTATE->m_fSongBeatNoOffset; - float fSongBeatVisible = GAMESTATE->m_fSongBeatVisible; - if( !m_pSoundMusic->IsPlaying() ) + if( m_pSoundMusic->IsPlaying() ) { - GAMESTATE->m_fSongBeat = m_fTrailingBeat; // put trailing beat in effect - GAMESTATE->m_fSongBeatNoOffset = m_fTrailingBeat; // put trailing beat in effect - GAMESTATE->m_fSongBeatVisible = m_fTrailingBeat; // put trailing beat in effect + ScreenWithMenuElements::DrawPrimitives(); + return; } + // HACK: Draw using the trailing beat + PlayerState *pPlayerState = const_cast (m_NoteFieldEdit.GetPlayerState()); + + float fPlayerSongBeat = pPlayerState->m_Position.m_fSongBeat; // save song beat + float fPlayerSongBeatNoOffset = pPlayerState->m_Position.m_fSongBeatNoOffset; + float fPlayerSongBeatVisible = pPlayerState->m_Position.m_fSongBeatVisible; + + float fGameSongBeat = GAMESTATE->m_Position.m_fSongBeat; // save song beat + float fGameSongBeatNoOffset = GAMESTATE->m_Position.m_fSongBeatNoOffset; + float fGameSongBeatVisible = GAMESTATE->m_Position.m_fSongBeatVisible; + + pPlayerState->m_Position.m_fSongBeat = m_fTrailingBeat; // put trailing beat in effect + pPlayerState->m_Position.m_fSongBeatNoOffset = m_fTrailingBeat; // put trailing beat in effect + pPlayerState->m_Position.m_fSongBeatVisible = m_fTrailingBeat; // put trailing beat in effect + + GAMESTATE->m_Position.m_fSongBeat = m_fTrailingBeat; // put trailing beat in effect + GAMESTATE->m_Position.m_fSongBeatNoOffset = m_fTrailingBeat; // put trailing beat in effect + GAMESTATE->m_Position.m_fSongBeatVisible = m_fTrailingBeat; // put trailing beat in effect + ScreenWithMenuElements::DrawPrimitives(); - if( !m_pSoundMusic->IsPlaying() ) - { - GAMESTATE->m_fSongBeat = fSongBeat; // restore real song beat - GAMESTATE->m_fSongBeatNoOffset = fSongBeatNoOffset; - GAMESTATE->m_fSongBeatVisible = fSongBeatVisible; - } + pPlayerState->m_Position.m_fSongBeat = fPlayerSongBeat; // restore real song beat + pPlayerState->m_Position.m_fSongBeatNoOffset = fPlayerSongBeatNoOffset; + pPlayerState->m_Position.m_fSongBeatVisible = fPlayerSongBeatVisible; + + GAMESTATE->m_Position.m_fSongBeat = fGameSongBeat; // restore real song beat + GAMESTATE->m_Position.m_fSongBeatNoOffset = fGameSongBeatNoOffset; + GAMESTATE->m_Position.m_fSongBeatVisible = fGameSongBeatVisible; + } void ScreenEdit::Input( const InputEventPlus &input ) @@ -1231,9 +1286,11 @@ static int FindAttackAtTime( const AttackArray& attacks, float fStartTime ) return -1; } +static LocalizedString BG_CHANGE_STEP_TIMING ( "ScreenEdit", "You must be in Song Timing Mode to edit BG Changes." ); + static LocalizedString SWITCHED_TO ( "ScreenEdit", "Switched to" ); static LocalizedString NO_BACKGROUNDS_AVAILABLE ( "ScreenEdit", "No backgrounds available" ); -static ThemeMetric INVERT_SCROLL_BUTTONS ( "ScreenEdit", "InvertScrollSpeedButtons" ); +static ThemeMetric INVERT_SCROLL_BUTTONS ( "ScreenEdit", "InvertScrollSpeedButtons" ); void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) { @@ -1243,7 +1300,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) m_iShiftAnchor = -1; return; } - int beatsPerMeasure = GAMESTATE->m_pCurSong->m_Timing.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_fSongBeat ).m_iNumerator; + int beatsPerMeasure = GAMESTATE->m_pCurSong->m_SongTiming.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat ).m_iNumerator; switch( EditB ) { @@ -1269,7 +1326,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) ShiftToRightSide( iCol, m_NoteDataEdit.GetNumTracks() ); - const float fSongBeat = GAMESTATE->m_fSongBeat; + const float fSongBeat = GetBeat(); const int iSongIndex = BeatToNoteRow( fSongBeat ); if( iCol >= m_NoteDataEdit.GetNumTracks() ) // this button is not in the range of columns for this Style @@ -1407,7 +1464,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) if( m_PlayerStateEdit.m_PlayerOptions.GetSong().m_fScrolls[PlayerOptions::SCROLL_REVERSE] > 0.5 ) fBeatsToMove *= -1; - float fDestinationBeat = GAMESTATE->m_fSongBeat + fBeatsToMove; + float fDestinationBeat = GetBeat() + fBeatsToMove; fDestinationBeat = Quantize( fDestinationBeat, NoteTypeToBeat(m_SnapDisplay.GetNoteType()) ); ScrollTo( fDestinationBeat ); @@ -1415,42 +1472,40 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) break; case EDIT_BUTTON_SCROLL_NEXT_MEASURE: { - float fDestinationBeat = GAMESTATE->m_fSongBeat + beatsPerMeasure; + float fDestinationBeat = GetBeat() + beatsPerMeasure; fDestinationBeat = ftruncf( fDestinationBeat, (float)beatsPerMeasure ); ScrollTo( fDestinationBeat ); break; } case EDIT_BUTTON_SCROLL_PREV_MEASURE: { - float fDestinationBeat = QuantizeUp( GAMESTATE->m_fSongBeat, (float)beatsPerMeasure ); + float fDestinationBeat = QuantizeUp( GetBeat(), (float)beatsPerMeasure ); fDestinationBeat -= (float)beatsPerMeasure; ScrollTo( fDestinationBeat ); break; } case EDIT_BUTTON_SCROLL_NEXT: { - int iRow = BeatToNoteRow( GAMESTATE->m_fSongBeat ); + int iRow = BeatToNoteRow( GetBeat() ); NoteDataUtil::GetNextEditorPosition( m_NoteDataEdit, iRow ); ScrollTo( NoteRowToBeat(iRow) ); } break; case EDIT_BUTTON_SCROLL_PREV: { - int iRow = BeatToNoteRow( GAMESTATE->m_fSongBeat ); + int iRow = BeatToNoteRow( GetBeat() ); NoteDataUtil::GetPrevEditorPosition( m_NoteDataEdit, iRow ); ScrollTo( NoteRowToBeat(iRow) ); } break; case EDIT_BUTTON_LABEL_NEXT: { - ScrollTo( GAMESTATE->m_pCurSong->m_Timing. - GetNextLabelSegmentBeatAtBeat( GAMESTATE->m_fSongBeat ) ); + ScrollTo( GetAppropriateTiming().GetNextLabelSegmentBeatAtBeat( GetBeat() ) ); } break; case EDIT_BUTTON_LABEL_PREV: { - ScrollTo( GAMESTATE->m_pCurSong->m_Timing. - GetPreviousLabelSegmentBeatAtBeat( GAMESTATE->m_fSongBeat ) ); + ScrollTo( GetAppropriateTiming().GetPreviousLabelSegmentBeatAtBeat( GetBeat() ) ); } break; case EDIT_BUTTON_SNAP_NEXT: @@ -1463,11 +1518,11 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) break; case EDIT_BUTTON_LAY_SELECT: { - const int iCurrentRow = BeatToNoteRow(GAMESTATE->m_fSongBeat); + const int iCurrentRow = BeatToNoteRow(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat); if( m_NoteFieldEdit.m_iBeginMarker==-1 && m_NoteFieldEdit.m_iEndMarker==-1 ) { // lay begin marker - m_NoteFieldEdit.m_iBeginMarker = BeatToNoteRow(GAMESTATE->m_fSongBeat); + m_NoteFieldEdit.m_iBeginMarker = BeatToNoteRow(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat); } else if( m_NoteFieldEdit.m_iEndMarker==-1 ) // only begin marker is laid { @@ -1516,6 +1571,11 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) case EDIT_BUTTON_OPEN_INPUT_HELP: DoHelp(); break; + case EDIT_BUTTON_OPEN_TIMING_MENU: + { + DisplayTimingMenu(); + break; + } case EDIT_BUTTON_OPEN_NEXT_STEPS: case EDIT_BUTTON_OPEN_PREV_STEPS: { @@ -1579,7 +1639,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) case EDIT_BUTTON_BPM_UP: case EDIT_BUTTON_BPM_DOWN: { - float fBPM = m_pSong->GetBPMAtBeat( GAMESTATE->m_fSongBeat ); + float fBPM = GetAppropriateTiming().GetBPMAtBeat( GetBeat() ); float fDelta; switch( EditB ) { @@ -1600,7 +1660,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) } float fNewBPM = fBPM + fDelta; - m_pSong->SetBPMAtBeat( GAMESTATE->m_fSongBeat, fNewBPM ); + GetAppropriateTiming().SetBPMAtBeat( GetBeat(), fNewBPM ); (fDelta>0 ? m_soundValueIncrease : m_soundValueDecrease).Play(); SetDirty( true ); } @@ -1627,24 +1687,24 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) fDelta *= 40; } unsigned i; - for( i=0; im_Timing.m_StopSegments.size(); i++ ) + for( i=0; im_Timing.m_StopSegments[i].m_iStartRow == BeatToNoteRow(GAMESTATE->m_fSongBeat) ) + if( GetAppropriateTiming().m_StopSegments[i].m_iStartRow == BeatToNoteRow( GetBeat() ) ) break; } - if( i == m_pSong->m_Timing.m_StopSegments.size() ) // there is no StopSegment at the current beat + if( i == GetAppropriateTiming().m_StopSegments.size() ) // there is no StopSegment at the current beat { // create a new StopSegment if( fDelta > 0 ) - m_pSong->AddStopSegment( StopSegment(BeatToNoteRow(GAMESTATE->m_fSongBeat), fDelta) ); + GetAppropriateTiming().AddStopSegment( StopSegment(BeatToNoteRow( GetBeat() ), fDelta) ); } - else // StopSegment being modified is m_Timing.m_StopSegments[i] + else // StopSegment being modified is m_SongTiming.m_StopSegments[i] { - m_pSong->m_Timing.m_StopSegments[i].m_fStopSeconds += fDelta; - if( m_pSong->m_Timing.m_StopSegments[i].m_fStopSeconds <= 0 ) - m_pSong->m_Timing.m_StopSegments.erase( m_pSong->m_Timing.m_StopSegments.begin()+i, - m_pSong->m_Timing.m_StopSegments.begin()+i+1); + GetAppropriateTiming().m_StopSegments[i].m_fStopSeconds += fDelta; + if( GetAppropriateTiming().m_StopSegments[i].m_fStopSeconds <= 0 ) + GetAppropriateTiming().m_StopSegments.erase( GetAppropriateTiming().m_StopSegments.begin()+i, + GetAppropriateTiming().m_StopSegments.begin()+i+1); } (fDelta>0 ? m_soundValueIncrease : m_soundValueDecrease).Play(); SetDirty( true ); @@ -1673,24 +1733,24 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) fDelta *= 40; } unsigned i; - for( i=0; im_Timing.m_StopSegments.size(); i++ ) + for( i=0; im_Timing.m_StopSegments[i].m_iStartRow == BeatToNoteRow(GAMESTATE->m_fSongBeat) ) + if( GetAppropriateTiming().m_StopSegments[i].m_iStartRow == BeatToNoteRow(GAMESTATE->m_Position.m_fSongBeat) ) break; } - if( i == m_pSong->m_Timing.m_StopSegments.size() ) // there is no delay segment at the current beat + if( i == GetAppropriateTiming().m_StopSegments.size() ) // there is no delay segment at the current beat { // create a new delay segment if( fDelta > 0 ) - m_pSong->AddStopSegment( StopSegment(BeatToNoteRow(GAMESTATE->m_fSongBeat), fDelta, true) ); + m_pSong->AddStopSegment( StopSegment(BeatToNoteRow(GAMESTATE->m_Position.m_fSongBeat), fDelta, true) ); } - else // delay segment being modified is m_Timing.m_StopSegments[i] + else // delay segment being modified is m_SongTiming.m_StopSegments[i] { - m_pSong->m_Timing.m_StopSegments[i].m_fStopSeconds += fDelta; - if( m_pSong->m_Timing.m_StopSegments[i].m_fStopSeconds <= 0 ) - m_pSong->m_Timing.m_StopSegments.erase( m_pSong->m_Timing.m_StopSegments.begin()+i, - m_pSong->m_Timing.m_StopSegments.begin()+i+1); + GetAppropriateTiming().m_StopSegments[i].m_fStopSeconds += fDelta; + if( GetAppropriateTiming().m_StopSegments[i].m_fStopSeconds <= 0 ) + GetAppropriateTiming().m_StopSegments.erase( GetAppropriateTiming().m_StopSegments.begin()+i, + GetAppropriateTiming().m_StopSegments.begin()+i+1); } (fDelta>0 ? m_soundValueIncrease : m_soundValueDecrease).Play(); SetDirty( true ); @@ -1718,7 +1778,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) else fDelta *= 40; } - m_pSong->m_Timing.m_fBeat0OffsetInSeconds += fDelta; + GetAppropriateTiming().m_fBeat0OffsetInSeconds += fDelta; (fDelta>0 ? m_soundValueIncrease : m_soundValueDecrease).Play(); SetDirty( true ); } @@ -1765,101 +1825,109 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) break; case EDIT_BUTTON_OPEN_BGCHANGE_LAYER1_MENU: case EDIT_BUTTON_OPEN_BGCHANGE_LAYER2_MENU: - switch( EditB ) + if( !GAMESTATE->m_bIsUsingStepTiming ) { - DEFAULT_FAIL( EditB ); - case EDIT_BUTTON_OPEN_BGCHANGE_LAYER1_MENU: g_CurrentBGChangeLayer = BACKGROUND_LAYER_1; break; - case EDIT_BUTTON_OPEN_BGCHANGE_LAYER2_MENU: g_CurrentBGChangeLayer = BACKGROUND_LAYER_2; break; - } - - { - // Fill in option names - vector vThrowAway; - - MenuDef &menu = g_BackgroundChange; - - menu.rows[layer].choices[0] = ssprintf("%d",g_CurrentBGChangeLayer); - BackgroundUtil::GetBackgroundTransitions( "", vThrowAway, menu.rows[transition].choices ); - g_BackgroundChange.rows[transition].choices.insert( g_BackgroundChange.rows[transition].choices.begin(), "None" ); // add "no transition" - BackgroundUtil::GetBackgroundEffects( "", vThrowAway, menu.rows[effect].choices ); - menu.rows[effect].choices.insert( menu.rows[effect].choices.begin(), "Default" ); // add "default effect" - - BackgroundUtil::GetSongBGAnimations( m_pSong, "", vThrowAway, menu.rows[file1_song_bganimation].choices ); - BackgroundUtil::GetSongMovies( m_pSong, "", vThrowAway, menu.rows[file1_song_movie].choices ); - BackgroundUtil::GetSongBitmaps( m_pSong, "", vThrowAway, menu.rows[file1_song_still].choices ); - BackgroundUtil::GetGlobalBGAnimations( m_pSong, "", vThrowAway, menu.rows[file1_global_bganimation].choices ); // NULL to get all background files - BackgroundUtil::GetGlobalRandomMovies( m_pSong, "", vThrowAway, menu.rows[file1_global_movie].choices, false, false ); // all backgrounds - BackgroundUtil::GetGlobalRandomMovies( m_pSong, "", vThrowAway, menu.rows[file1_global_movie_song_group].choices, false, true ); // song group's backgrounds - BackgroundUtil::GetGlobalRandomMovies( m_pSong, "", vThrowAway, menu.rows[file1_global_movie_song_group_and_genre].choices, true, true ); // song group and genre's backgrounds - - menu.rows[file2_type].choices = menu.rows[file1_type].choices; - menu.rows[file2_song_bganimation].choices = menu.rows[file1_song_bganimation].choices; - menu.rows[file2_song_movie].choices = menu.rows[file1_song_movie].choices; - menu.rows[file2_song_still].choices = menu.rows[file1_song_still].choices; - menu.rows[file2_global_bganimation].choices = menu.rows[file1_global_bganimation].choices; - menu.rows[file2_global_movie].choices = menu.rows[file1_global_movie].choices; - menu.rows[file2_global_movie_song_group].choices = menu.rows[file1_global_movie_song_group].choices; - menu.rows[file2_global_movie_song_group_and_genre].choices = menu.rows[file1_global_movie_song_group_and_genre].choices; - - - // Fill in lines enabled/disabled - bool bAlreadyBGChangeHere = false; - BackgroundChange bgChange; - FOREACH( BackgroundChange, m_pSong->GetBackgroundChanges(g_CurrentBGChangeLayer), bgc ) + switch( EditB ) { - if( bgc->m_fStartBeat == GAMESTATE->m_fSongBeat ) - { - bAlreadyBGChangeHere = true; - bgChange = *bgc; - } + DEFAULT_FAIL( EditB ); + case EDIT_BUTTON_OPEN_BGCHANGE_LAYER1_MENU: g_CurrentBGChangeLayer = BACKGROUND_LAYER_1; break; + case EDIT_BUTTON_OPEN_BGCHANGE_LAYER2_MENU: g_CurrentBGChangeLayer = BACKGROUND_LAYER_2; break; } + { + // Fill in option names + vector vThrowAway; + + MenuDef &menu = g_BackgroundChange; + + menu.rows[layer].choices[0] = ssprintf("%d",g_CurrentBGChangeLayer); + BackgroundUtil::GetBackgroundTransitions( "", vThrowAway, menu.rows[transition].choices ); + g_BackgroundChange.rows[transition].choices.insert( g_BackgroundChange.rows[transition].choices.begin(), "None" ); // add "no transition" + BackgroundUtil::GetBackgroundEffects( "", vThrowAway, menu.rows[effect].choices ); + menu.rows[effect].choices.insert( menu.rows[effect].choices.begin(), "Default" ); // add "default effect" + + BackgroundUtil::GetSongBGAnimations( m_pSong, "", vThrowAway, menu.rows[file1_song_bganimation].choices ); + BackgroundUtil::GetSongMovies( m_pSong, "", vThrowAway, menu.rows[file1_song_movie].choices ); + BackgroundUtil::GetSongBitmaps( m_pSong, "", vThrowAway, menu.rows[file1_song_still].choices ); + BackgroundUtil::GetGlobalBGAnimations( m_pSong, "", vThrowAway, menu.rows[file1_global_bganimation].choices ); // NULL to get all background files + BackgroundUtil::GetGlobalRandomMovies( m_pSong, "", vThrowAway, menu.rows[file1_global_movie].choices, false, false ); // all backgrounds + BackgroundUtil::GetGlobalRandomMovies( m_pSong, "", vThrowAway, menu.rows[file1_global_movie_song_group].choices, false, true ); // song group's backgrounds + BackgroundUtil::GetGlobalRandomMovies( m_pSong, "", vThrowAway, menu.rows[file1_global_movie_song_group_and_genre].choices, true, true ); // song group and genre's backgrounds + + menu.rows[file2_type].choices = menu.rows[file1_type].choices; + menu.rows[file2_song_bganimation].choices = menu.rows[file1_song_bganimation].choices; + menu.rows[file2_song_movie].choices = menu.rows[file1_song_movie].choices; + menu.rows[file2_song_still].choices = menu.rows[file1_song_still].choices; + menu.rows[file2_global_bganimation].choices = menu.rows[file1_global_bganimation].choices; + menu.rows[file2_global_movie].choices = menu.rows[file1_global_movie].choices; + menu.rows[file2_global_movie_song_group].choices = menu.rows[file1_global_movie_song_group].choices; + menu.rows[file2_global_movie_song_group_and_genre].choices = menu.rows[file1_global_movie_song_group_and_genre].choices; + + + // Fill in lines enabled/disabled + bool bAlreadyBGChangeHere = false; + BackgroundChange bgChange; + FOREACH( BackgroundChange, m_pSong->GetBackgroundChanges(g_CurrentBGChangeLayer), bgc ) + { + if( bgc->m_fStartBeat == GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat ) + { + bAlreadyBGChangeHere = true; + bgChange = *bgc; + } + } + #define FILL_ENABLED( x ) menu.rows[x].bEnabled = menu.rows[x].choices.size() > 0; - FILL_ENABLED( transition ); - FILL_ENABLED( effect ); - FILL_ENABLED( file1_song_bganimation ); - FILL_ENABLED( file1_song_movie ); - FILL_ENABLED( file1_song_still ); - FILL_ENABLED( file1_global_bganimation ); - FILL_ENABLED( file1_global_movie ); - FILL_ENABLED( file1_global_movie_song_group ); - FILL_ENABLED( file1_global_movie_song_group_and_genre ); - FILL_ENABLED( file2_song_bganimation ); - FILL_ENABLED( file2_song_movie ); - FILL_ENABLED( file2_song_still ); - FILL_ENABLED( file2_global_bganimation ); - FILL_ENABLED( file2_global_movie ); - FILL_ENABLED( file2_global_movie_song_group ); - FILL_ENABLED( file2_global_movie_song_group_and_genre ); + FILL_ENABLED( transition ); + FILL_ENABLED( effect ); + FILL_ENABLED( file1_song_bganimation ); + FILL_ENABLED( file1_song_movie ); + FILL_ENABLED( file1_song_still ); + FILL_ENABLED( file1_global_bganimation ); + FILL_ENABLED( file1_global_movie ); + FILL_ENABLED( file1_global_movie_song_group ); + FILL_ENABLED( file1_global_movie_song_group_and_genre ); + FILL_ENABLED( file2_song_bganimation ); + FILL_ENABLED( file2_song_movie ); + FILL_ENABLED( file2_song_still ); + FILL_ENABLED( file2_global_bganimation ); + FILL_ENABLED( file2_global_movie ); + FILL_ENABLED( file2_global_movie_song_group ); + FILL_ENABLED( file2_global_movie_song_group_and_genre ); #undef FILL_ENABLED - menu.rows[delete_change].bEnabled = bAlreadyBGChangeHere; + menu.rows[delete_change].bEnabled = bAlreadyBGChangeHere; - // set default choices - menu.rows[rate]. SetDefaultChoiceIfPresent( ssprintf("%2.0f%%",bgChange.m_fRate*100) ); - menu.rows[transition]. SetDefaultChoiceIfPresent( bgChange.m_sTransition ); - menu.rows[effect]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sEffect ); - menu.rows[file1_type].iDefaultChoice = none; - if( bgChange.m_def.m_sFile1 == RANDOM_BACKGROUND_FILE ) menu.rows[file1_type].iDefaultChoice = dynamic_random; - if( menu.rows[file1_song_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = song_bganimation; - if( menu.rows[file1_song_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = song_movie; - if( menu.rows[file1_song_still]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = song_bitmap; - if( menu.rows[file1_global_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_bganimation; - if( menu.rows[file1_global_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_movie; - if( menu.rows[file1_global_movie_song_group]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_movie_song_group; - if( menu.rows[file1_global_movie_song_group_and_genre]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_movie_song_group_and_genre; - menu.rows[file2_type].iDefaultChoice = none; - if( bgChange.m_def.m_sFile2 == RANDOM_BACKGROUND_FILE ) menu.rows[file2_type].iDefaultChoice = dynamic_random; - if( menu.rows[file2_song_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = song_bganimation; - if( menu.rows[file2_song_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = song_movie; - if( menu.rows[file2_song_still]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = song_bitmap; - if( menu.rows[file2_global_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_bganimation; - if( menu.rows[file2_global_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_movie; - if( menu.rows[file2_global_movie_song_group]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_movie_song_group; - if( menu.rows[file2_global_movie_song_group_and_genre]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_movie_song_group_and_genre; - menu.rows[color1]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sColor1 ); - menu.rows[color2]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sColor2 ); + // set default choices + menu.rows[rate]. SetDefaultChoiceIfPresent( ssprintf("%2.0f%%",bgChange.m_fRate*100) ); + menu.rows[transition]. SetDefaultChoiceIfPresent( bgChange.m_sTransition ); + menu.rows[effect]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sEffect ); + menu.rows[file1_type].iDefaultChoice = none; + if( bgChange.m_def.m_sFile1 == RANDOM_BACKGROUND_FILE ) menu.rows[file1_type].iDefaultChoice = dynamic_random; + if( menu.rows[file1_song_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = song_bganimation; + if( menu.rows[file1_song_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = song_movie; + if( menu.rows[file1_song_still]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = song_bitmap; + if( menu.rows[file1_global_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_bganimation; + if( menu.rows[file1_global_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_movie; + if( menu.rows[file1_global_movie_song_group]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_movie_song_group; + if( menu.rows[file1_global_movie_song_group_and_genre]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile1 ) ) menu.rows[file1_type].iDefaultChoice = global_movie_song_group_and_genre; + menu.rows[file2_type].iDefaultChoice = none; + if( bgChange.m_def.m_sFile2 == RANDOM_BACKGROUND_FILE ) menu.rows[file2_type].iDefaultChoice = dynamic_random; + if( menu.rows[file2_song_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = song_bganimation; + if( menu.rows[file2_song_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = song_movie; + if( menu.rows[file2_song_still]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = song_bitmap; + if( menu.rows[file2_global_bganimation]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_bganimation; + if( menu.rows[file2_global_movie]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_movie; + if( menu.rows[file2_global_movie_song_group]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_movie_song_group; + if( menu.rows[file2_global_movie_song_group_and_genre]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sFile2 ) ) menu.rows[file2_type].iDefaultChoice = global_movie_song_group_and_genre; + menu.rows[color1]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sColor1 ); + menu.rows[color2]. SetDefaultChoiceIfPresent( bgChange.m_def.m_sColor2 ); - EditMiniMenu( &g_BackgroundChange, SM_BackFromBGChange ); + EditMiniMenu( &g_BackgroundChange, SM_BackFromBGChange ); + } + } + else + { + SCREENMAN->SystemMessage( BG_CHANGE_STEP_TIMING ); + SCREENMAN->PlayInvalidSound(); } break; @@ -1896,11 +1964,12 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) break; case EDIT_BUTTON_OPEN_COURSE_ATTACK_MENU: { + // TODO: Give Song/Step Timing switches/functions here? Course *pCourse = GAMESTATE->m_pCurCourse; if( pCourse == NULL ) break; CourseEntry &ce = pCourse->m_vEntries[GAMESTATE->m_iEditCourseEntryIndex]; - float fStartTime = m_pSong->GetElapsedTimeFromBeat( GAMESTATE->m_fSongBeat ); + float fStartTime = m_pSteps->m_Timing.GetElapsedTimeFromBeat( GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat ); int iAttack = FindAttackAtTime( ce.attacks, fStartTime ); if( iAttack >= 0 ) @@ -1941,7 +2010,8 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) } else { - fStart = m_pSong->GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iBeginMarker) ); + // TODO: Give Song/Step Timing switches/functions here? + fStart = m_pSteps->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iBeginMarker) ); int iAttack = FindAttackAtTime( ce.attacks, fStart ); if( iAttack >= 0 ) @@ -1950,7 +2020,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) if( m_NoteFieldEdit.m_iEndMarker == -1 ) fEnd = m_pSong->m_fMusicLengthSeconds; else - fEnd = m_pSong->GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iEndMarker) ); + fEnd = m_pSteps->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iEndMarker) ); } g_fLastInsertAttackPositionSeconds = fStart; g_fLastInsertAttackDurationSeconds = fEnd - fStart; @@ -1974,10 +2044,10 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) bool bAlreadyBGChangeHere = false; BackgroundLayer iLayer = BACKGROUND_LAYER_1; BackgroundChange bgChange; - bgChange.m_fStartBeat = GAMESTATE->m_fSongBeat; + bgChange.m_fStartBeat = GAMESTATE->m_Position.m_fSongBeat; FOREACH( BackgroundChange, m_pSong->GetBackgroundChanges(iLayer), bgc ) { - if( bgc->m_fStartBeat == GAMESTATE->m_fSongBeat ) + if( bgc->m_fStartBeat == GAMESTATE->m_Position.m_fSongBeat ) { bAlreadyBGChangeHere = true; bgChange = *bgc; @@ -2012,12 +2082,12 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) { if( g_iDefaultRecordLength.Get() == -1 ) { - m_iStartPlayingAt = BeatToNoteRow(GAMESTATE->m_fSongBeat); + m_iStartPlayingAt = BeatToNoteRow(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat); m_iStopPlayingAt = max( m_iStartPlayingAt, m_NoteDataEdit.GetLastRow() + 1 ); } else { - m_iStartPlayingAt = BeatToNoteRow( ftruncf(GAMESTATE->m_fSongBeat, g_iDefaultRecordLength.Get()) ); + m_iStartPlayingAt = BeatToNoteRow( ftruncf(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat, g_iDefaultRecordLength.Get()) ); m_iStopPlayingAt = m_iStartPlayingAt + BeatToNoteRow( g_iDefaultRecordLength.Get() ); } @@ -2034,7 +2104,7 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) } break; case EDIT_BUTTON_RECORD_FROM_CURSOR: - m_iStartPlayingAt = BeatToNoteRow(GAMESTATE->m_fSongBeat); + m_iStartPlayingAt = BeatToNoteRow(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat); m_iStopPlayingAt = max( m_iStartPlayingAt, m_NoteDataEdit.GetLastRow() ); TransitionEditState( STATE_RECORDING ); break; @@ -2073,8 +2143,14 @@ void ScreenEdit::InputEdit( const InputEventPlus &input, EditButton EditB ) enum_add( m_InputPlayerNumber, 1 ); if( m_InputPlayerNumber == NUM_PLAYERS ) m_InputPlayerNumber = PLAYER_1; + m_soundSwitchPlayer.Play(); } break; + + case EDIT_BUTTON_SWITCH_TIMINGS: + GAMESTATE->m_bIsUsingStepTiming = !GAMESTATE->m_bIsUsingStepTiming; + m_soundSwitchTiming.Play(); + break; } } @@ -2103,7 +2179,7 @@ void ScreenEdit::InputRecord( const InputEventPlus &input, EditButton EditB ) } // Add a tap - float fBeat = GAMESTATE->m_fSongBeat; + float fBeat = GetBeat(); fBeat = Quantize( fBeat, NoteTypeToBeat(m_SnapDisplay.GetNoteType()) ); const int iRow = BeatToNoteRow( fBeat ); @@ -2239,7 +2315,7 @@ void ScreenEdit::InputPlay( const InputEventPlus &input, EditButton EditB ) { case EDIT_BUTTON_RETURN_TO_EDIT: /* When exiting play mode manually, leave the cursor where it is. */ - m_fBeatToReturnTo = GAMESTATE->m_fSongBeat; + m_fBeatToReturnTo = GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat; TransitionEditState( STATE_EDITING ); break; case EDIT_BUTTON_OFFSET_UP: @@ -2265,7 +2341,7 @@ void ScreenEdit::InputPlay( const InputEventPlus &input, EditButton EditB ) fOffsetDelta *= 40; } - m_pSong->m_Timing.m_fBeat0OffsetInSeconds += fOffsetDelta; + GetAppropriateTiming().m_fBeat0OffsetInSeconds += fOffsetDelta; } break; } @@ -2310,7 +2386,7 @@ void ScreenEdit::TransitionEditState( EditState em ) { case STATE_EDITING: // If exiting EDIT mode, save the cursor position. - m_fBeatToReturnTo = GAMESTATE->m_fSongBeat; + m_fBeatToReturnTo = GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat; break; case STATE_PLAYING: @@ -2354,16 +2430,9 @@ void ScreenEdit::TransitionEditState( EditState em ) m_Background.Unload(); m_Foreground.Unload(); - // Restore the cursor position. - GAMESTATE->m_fSongBeat = m_fBeatToReturnTo; - - // Make sure we're snapped. - GAMESTATE->m_fSongBeat = Quantize( GAMESTATE->m_fSongBeat, NoteTypeToBeat(m_SnapDisplay.GetNoteType()) ); - - /* Playing and recording have lead-ins, which may start before beat 0; - * make sure we don't stay there if we escaped out early. */ - GAMESTATE->m_fSongBeat = max( GAMESTATE->m_fSongBeat, 0 ); - + // Restore the cursor position + Quantize + Clamp + SetBeat( max( 0, Quantize( m_fBeatToReturnTo, NoteTypeToBeat(m_SnapDisplay.GetNoteType()) ) ) ); + GAMESTATE->m_bInStepEditor = true; break; case STATE_PLAYING: @@ -2372,9 +2441,10 @@ void ScreenEdit::TransitionEditState( EditState em ) if( bStateChanging ) AdjustSync::ResetOriginalSyncData(); - /* Give a 1 second lead-in. If we're loading Player, this must be done first. */ - float fSeconds = m_pSong->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(m_iStartPlayingAt) ) - 1; - GAMESTATE->UpdateSongPosition( fSeconds, m_pSong->m_Timing ); + /* Give a 1 second lead-in. If we're loading Player, this must be done first. + * Also be sure to get the right timing. */ + float fSeconds = GetAppropriateTiming().GetElapsedTimeFromBeat( NoteRowToBeat(m_iStartPlayingAt) ) - 1; + GAMESTATE->UpdateSongPosition( fSeconds, GetAppropriateTiming(), RageZeroTimer, true ); GAMESTATE->m_bGameplayLeadIn.Set( false ); @@ -2382,10 +2452,11 @@ void ScreenEdit::TransitionEditState( EditState em ) // XXX // GAMESTATE->ResetNoteSkins(); //GAMESTATE->res - + GAMESTATE->m_bInStepEditor = false; break; } case STATE_RECORDING_PAUSED: + GAMESTATE->m_bInStepEditor = false; break; } @@ -2455,7 +2526,7 @@ void ScreenEdit::TransitionEditState( EditState em ) { case STATE_PLAYING: case STATE_RECORDING: - const float fStartSeconds = m_pSong->GetElapsedTimeFromBeat(GAMESTATE->m_fSongBeat); + const float fStartSeconds = GetAppropriateTiming().GetElapsedTimeFromBeat( GetBeat() ); LOG->Trace( "Starting playback at %f", fStartSeconds ); RageSoundParams p; @@ -2475,11 +2546,11 @@ void ScreenEdit::ScrollTo( float fDestinationBeat ) CLAMP( fDestinationBeat, 0, GetMaximumBeatForMoving() ); // Don't play the sound and do the hold note logic below if our position didn't change. - const float fOriginalBeat = GAMESTATE->m_fSongBeat; + const float fOriginalBeat = GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat; if( fOriginalBeat == fDestinationBeat ) return; - GAMESTATE->m_fSongBeat = fDestinationBeat; + SetBeat(fDestinationBeat); // check to see if they're holding a button for( int n=0; n 0 ) - m_pSong->SetBPMAtBeat( GAMESTATE->m_fSongBeat, fBPM ); + GetAppropriateTiming().SetBPMAtBeat( GetBeat(), fBPM ); SetDirty( true ); } else if( SM == SM_BackFromStopChange ) { float fStop = StringToFloat( ScreenTextEntry::s_sLastAnswer ); if( fStop >= 0 ) - m_pSong->m_Timing.SetStopAtBeat( GAMESTATE->m_fSongBeat, fStop ); + GetAppropriateTiming().SetStopAtBeat( GetBeat(), fStop ); SetDirty( true ); } else if( SM == SM_BackFromDelayChange ) { float fDelay = StringToFloat( ScreenTextEntry::s_sLastAnswer ); if( fDelay >= 0 ) - m_pSong->m_Timing.SetStopAtBeat( GAMESTATE->m_fSongBeat, fDelay, true ); + GetAppropriateTiming().SetStopAtBeat( GetBeat(), fDelay, true ); SetDirty( true ); } - else if( SM == SM_BackFromTimeSignatureNumeratorChange ) + else if( SM == SM_BackFromTimeSignatureChange ) { - int iNum = StringToInt( ScreenTextEntry::s_sLastAnswer ); - if( iNum > 0 ) + int iNum, iDen; + if( sscanf( ScreenTextEntry::s_sLastAnswer.c_str(), " %d / %d ", &iNum, &iDen ) == 2 ) { - m_pSong->m_Timing.SetTimeSignatureNumeratorAtBeat( GAMESTATE->m_fSongBeat, iNum ); - } - SetDirty( true ); - } - else if ( SM == SM_BackFromTimeSignatureDenominatorChange ) - { - int iDen = StringToInt( ScreenTextEntry::s_sLastAnswer ); - if( iDen > 0) - { - m_pSong->m_Timing.SetTimeSignatureDenominatorAtBeat( GAMESTATE->m_fSongBeat, iDen ); + GetAppropriateTiming().SetTimeSignatureAtBeat( GetBeat(), iNum, iDen ); } SetDirty( true ); } @@ -2657,7 +2719,7 @@ void ScreenEdit::HandleScreenMessage( const ScreenMessage SM ) int iTick = StringToInt( ScreenTextEntry::s_sLastAnswer ); if ( iTick >= 0 && iTick <= ROWS_PER_BEAT ) { - m_pSong->m_Timing.SetTickcountAtBeat( GAMESTATE->m_fSongBeat, iTick ); + GetAppropriateTiming().SetTickcountAtBeat( GetBeat(), iTick ); } SetDirty( true ); } @@ -2666,27 +2728,79 @@ void ScreenEdit::HandleScreenMessage( const ScreenMessage SM ) int iCombo = StringToInt( ScreenTextEntry::s_sLastAnswer ); if ( iCombo >= 0 ) { - m_pSong->m_Timing.SetComboAtBeat( GAMESTATE->m_fSongBeat, iCombo ); + GetAppropriateTiming().SetComboAtBeat( GetBeat(), iCombo ); } SetDirty( true ); } else if ( SM == SM_BackFromLabelChange ) { RString sLabel = ScreenTextEntry::s_sLastAnswer; - if ( !m_pSong->m_Timing.DoesLabelExist(sLabel) ) + if ( !GetAppropriateTiming().DoesLabelExist(sLabel) ) { sLabel.Replace("=", "_"); sLabel.Replace(",", "_"); - m_pSong->m_Timing.SetLabelAtBeat( GAMESTATE->m_fSongBeat, sLabel ); + GetAppropriateTiming().SetLabelAtBeat( GetBeat(), sLabel ); SetDirty( true ); } } else if ( SM == SM_BackFromWarpChange ) { float fWarp = StringToFloat( ScreenTextEntry::s_sLastAnswer ); - m_pSong->m_Timing.SetWarpAtBeat( GAMESTATE->m_fSongBeat, fWarp ); + if( fWarp >= 0 ) // allow 0 to kill a warp. + { + GetAppropriateTiming().SetWarpAtBeat( GetBeat(), fWarp ); + SetDirty( true ); + } + } + else if( SM == SM_BackFromSpeedPercentChange ) + { + float fNum = StringToFloat( ScreenTextEntry::s_sLastAnswer ); + GetAppropriateTiming().SetSpeedPercentAtBeat( GetBeat(), fNum ); SetDirty( true ); } + else if ( SM == SM_BackFromSpeedWaitChange ) + { + float fDen = StringToFloat( ScreenTextEntry::s_sLastAnswer ); + if( fDen >= 0) + { + GetAppropriateTiming().SetSpeedWaitAtBeat( GetBeat(), fDen ); + } + SetDirty( true ); + } + else if ( SM == SM_BackFromSpeedModeChange ) + { + if( ScreenTextEntry::s_sLastAnswer.substr(0, 1) == "b" || ScreenTextEntry::s_sLastAnswer.substr(0, 1) == "B" ) + { + GetAppropriateTiming().SetSpeedModeAtBeat( GetBeat(), 0 ); + } + else if( ScreenTextEntry::s_sLastAnswer.substr(0, 1) == "s" || ScreenTextEntry::s_sLastAnswer.substr(0, 1) == "S" ) + { + GetAppropriateTiming().SetSpeedModeAtBeat( GetBeat(), 1 ); + } + else + { + int tmp = StringToInt(ScreenTextEntry::s_sLastAnswer ); + if( tmp == 0 ) + { + GetAppropriateTiming().SetSpeedModeAtBeat( GetBeat(), 0 ); + } + else + { + GetAppropriateTiming().SetSpeedModeAtBeat( GetBeat(), 1 ); + } + } + SetDirty( true ); + } + else if ( SM == SM_BackFromFakeChange ) + { + float fFake = StringToFloat( ScreenTextEntry::s_sLastAnswer ); + if( fFake >= 0 ) // allow 0 to kill a warp. + { + GetAppropriateTiming().SetFakeAtBeat( GetBeat(), fFake ); + SetDirty( true ); + } + } + else if( SM == SM_BackFromBGChange ) { HandleBGChangeChoice( (BGChangeChoice)ScreenMiniMenu::s_iLastRowCode, ScreenMiniMenu::s_viLastAnswers ); @@ -2735,7 +2849,7 @@ void ScreenEdit::HandleScreenMessage( const ScreenMessage SM ) { PlayerOptions poChosen = GAMESTATE->m_pPlayerState[PLAYER_1]->m_PlayerOptions.GetPreferred(); RString sMods = poChosen.GetString(); - const int row = BeatToNoteRow( GAMESTATE->m_fSongBeat ); + const int row = BeatToNoteRow( GAMESTATE->m_Position.m_fSongBeat ); TapNote tn( TapNote::attack, @@ -2756,8 +2870,10 @@ void ScreenEdit::HandleScreenMessage( const ScreenMessage SM ) Course *pCourse = GAMESTATE->m_pCurCourse; CourseEntry &ce = pCourse->m_vEntries[GAMESTATE->m_iEditCourseEntryIndex]; int iAttack; + + // TODO: Handle Song/Step Timing functions/switches here? - g_fLastInsertAttackPositionSeconds = m_pSong->GetElapsedTimeFromBeat( GAMESTATE->m_fSongBeat ); + g_fLastInsertAttackPositionSeconds = m_pSteps->m_Timing.GetElapsedTimeFromBeat( GAMESTATE->m_Position.m_fSongBeat ); g_fLastInsertAttackDurationSeconds = StringToFloat( g_InsertCourseAttack.rows[0].choices[iDurationChoice] ); iAttack = FindAttackAtTime( ce.attacks, g_fLastInsertAttackPositionSeconds ); @@ -2825,6 +2941,15 @@ void ScreenEdit::HandleScreenMessage( const ScreenMessage SM ) SetDirty( false ); } } + else if( SM == SM_DoEraseStepTiming ) + { + if( ScreenPrompt::s_LastAnswer == ANSWER_YES ) + { + SaveUndo(); + m_pSteps->m_Timing = m_pSong->m_SongTiming; + SetDirty( true ); + } + } else if( SM == SM_DoSaveAndExit ) // just asked "save before exiting? yes, no, cancel" { switch( ScreenPrompt::s_LastAnswer ) @@ -2912,12 +3037,12 @@ void ScreenEdit::HandleScreenMessage( const ScreenMessage SM ) { /* When another screen comes up, RageSounds takes over the sound timer. When we come * back, put the timer back to where it was. */ - GAMESTATE->m_fSongBeat = m_fTrailingBeat; + SetBeat(m_fTrailingBeat); } else if( SM == SM_LoseFocus ) { // Snap the trailing beat, in case we lose focus while tweening. - m_fTrailingBeat = GAMESTATE->m_fSongBeat; + m_fTrailingBeat = GetBeat(); //GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat; } ScreenWithMenuElements::HandleScreenMessage( SM ); @@ -2928,10 +3053,10 @@ void ScreenEdit::OnSnapModeChange() m_soundChangeSnap.Play(); NoteType nt = m_SnapDisplay.GetNoteType(); - int iStepIndex = BeatToNoteRow( GAMESTATE->m_fSongBeat ); + int iStepIndex = BeatToNoteRow( GetBeat() ); int iElementsPerNoteType = BeatToNoteRow( NoteTypeToBeat(nt) ); int iStepIndexHangover = iStepIndex % iElementsPerNoteType; - GAMESTATE->m_fSongBeat -= NoteRowToBeat( iStepIndexHangover ); + SetBeat( GetBeat() - NoteRowToBeat( iStepIndexHangover ) ); } @@ -3014,7 +3139,8 @@ static void ChangeArtistTranslit( const RString &sNew ) static void ChangeBeat0Offset( const RString &sNew ) { - GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds = StringToFloat( sNew ); + TimingData &timing = GAMESTATE->m_bIsUsingStepTiming ? GAMESTATE->m_pCurSteps[PLAYER_1]->m_Timing : GAMESTATE->m_pCurSong->m_SongTiming; + timing.m_fBeat0OffsetInSeconds = StringToFloat( sNew ); } static void ChangeLastBeatHint( const RString &sNew ) @@ -3042,6 +3168,69 @@ static void ChangeMaxBPM( const RString &sNew ) GAMESTATE->m_pCurSong->m_fSpecifiedBPMMax = StringToFloat( sNew ); } +TimingData & ScreenEdit::GetAppropriateTiming() const +{ + if( GAMESTATE->m_bIsUsingStepTiming ) + { + return m_pSteps->m_Timing; + } + return m_pSong->m_SongTiming; +} + +inline void ScreenEdit::SetBeat(float fBeat) +{ + if( !GAMESTATE->m_bIsUsingStepTiming ) + { + GAMESTATE->m_Position.m_fSongBeat = fBeat; + GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat = m_pSteps->m_Timing.GetBeatFromElapsedTime(m_pSong->m_SongTiming.GetElapsedTimeFromBeat(fBeat)); + } + else + { + GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat = fBeat; + GAMESTATE->m_Position.m_fSongBeat = m_pSong->m_SongTiming.GetBeatFromElapsedTime(m_pSteps->m_Timing.GetElapsedTimeFromBeat(fBeat)); + } +} + +inline float ScreenEdit::GetBeat() +{ + if( !GAMESTATE->m_bIsUsingStepTiming ) + { + return GAMESTATE->m_Position.m_fSongBeat; + } + return GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat; +} + +void ScreenEdit::DisplayTimingMenu() +{ + float fBeat = GetBeat(); + TimingData &pTime = GetAppropriateTiming(); + g_TimingDataInformation.rows[beat_0_offset].SetOneUnthemedChoice( ssprintf("%.5f", pTime.m_fBeat0OffsetInSeconds) ); + g_TimingDataInformation.rows[bpm].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetBPMAtBeat( fBeat ) ) ); + g_TimingDataInformation.rows[stop].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetStopAtBeat( fBeat ) ) ) ; + g_TimingDataInformation.rows[delay].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetDelayAtBeat( fBeat ) ) ); + g_TimingDataInformation.rows[time_signature].SetOneUnthemedChoice( ssprintf("%d / %d", pTime.GetTimeSignatureNumeratorAtBeat( fBeat ), pTime.GetTimeSignatureDenominatorAtBeat( fBeat ) ) ); + g_TimingDataInformation.rows[label].SetOneUnthemedChoice( pTime.GetLabelAtBeat( fBeat ).c_str() ); + g_TimingDataInformation.rows[tickcount].SetOneUnthemedChoice( ssprintf("%d", pTime.GetTickcountAtBeat( fBeat ) ) ); + g_TimingDataInformation.rows[combo].SetOneUnthemedChoice( ssprintf("%d", pTime.GetComboAtBeat( fBeat ) ) ); + g_TimingDataInformation.rows[warp].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetWarpAtBeat( fBeat ) ) ); + g_TimingDataInformation.rows[speed_percent].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetSpeedPercentAtBeat( fBeat ) ) ); + g_TimingDataInformation.rows[speed_wait].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetSpeedWaitAtBeat( fBeat ) ) ); + + RString starting = ( pTime.GetSpeedModeAtBeat( fBeat ) == 1 ? "Seconds" : "Beats" ); + g_TimingDataInformation.rows[speed_mode].SetOneUnthemedChoice( starting.c_str() ); + + g_TimingDataInformation.rows[fake].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetFakeAtBeat( fBeat ) ) ); + + g_TimingDataInformation.rows[tickcount].bEnabled = GAMESTATE->m_bIsUsingStepTiming; + g_TimingDataInformation.rows[combo].bEnabled = GAMESTATE->m_bIsUsingStepTiming; + g_TimingDataInformation.rows[speed_percent].bEnabled = GAMESTATE->m_bIsUsingStepTiming; + g_TimingDataInformation.rows[speed_wait].bEnabled = GAMESTATE->m_bIsUsingStepTiming; + g_TimingDataInformation.rows[speed_mode].bEnabled = GAMESTATE->m_bIsUsingStepTiming; + g_TimingDataInformation.rows[fake].bEnabled = GAMESTATE->m_bIsUsingStepTiming; + + EditMiniMenu( &g_TimingDataInformation, SM_BackFromTimingDataInformation ); +} + // End helper functions static LocalizedString REVERT_LAST_SAVE ( "ScreenEdit", "Do you want to revert to your last save?" ); @@ -3078,14 +3267,14 @@ void ScreenEdit::HandleMainMenuChoice( MainMenuChoice c, const vector &iAns break; case play_current_beat_to_end: { - m_iStartPlayingAt = BeatToNoteRow(GAMESTATE->m_fSongBeat); + m_iStartPlayingAt = BeatToNoteRow(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat); m_iStopPlayingAt = max( m_iStartPlayingAt, m_NoteDataEdit.GetLastRow() ); TransitionEditState( STATE_PLAYING ); } break; case set_selection_start: { - const int iCurrentRow = BeatToNoteRow(GAMESTATE->m_fSongBeat); + const int iCurrentRow = BeatToNoteRow(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat); if( m_NoteFieldEdit.m_iEndMarker!=-1 && iCurrentRow >= m_NoteFieldEdit.m_iEndMarker ) { SCREENMAN->PlayInvalidSound(); @@ -3099,7 +3288,7 @@ void ScreenEdit::HandleMainMenuChoice( MainMenuChoice c, const vector &iAns break; case set_selection_end: { - const int iCurrentRow = BeatToNoteRow(GAMESTATE->m_fSongBeat); + const int iCurrentRow = BeatToNoteRow(GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat); if( m_NoteFieldEdit.m_iBeginMarker!=-1 && iCurrentRow <= m_NoteFieldEdit.m_iBeginMarker ) { SCREENMAN->PlayInvalidSound(); @@ -3232,7 +3421,6 @@ void ScreenEdit::HandleMainMenuChoice( MainMenuChoice c, const vector &iAns g_SongInformation.rows[main_title_transliteration].SetOneUnthemedChoice( pSong->m_sMainTitleTranslit ); g_SongInformation.rows[sub_title_transliteration].SetOneUnthemedChoice( pSong->m_sSubTitleTranslit ); g_SongInformation.rows[artist_transliteration].SetOneUnthemedChoice( pSong->m_sArtistTranslit ); - g_SongInformation.rows[beat_0_offset].SetOneUnthemedChoice( ssprintf("%.5f", pSong->m_Timing.m_fBeat0OffsetInSeconds) ); g_SongInformation.rows[last_beat_hint].SetOneUnthemedChoice( ssprintf("%.5f", pSong->m_fSpecifiedLastBeat) ); g_SongInformation.rows[preview_start].SetOneUnthemedChoice( ssprintf("%.5f", pSong->m_fMusicSampleStartSeconds) ); g_SongInformation.rows[preview_length].SetOneUnthemedChoice( ssprintf("%.5f", pSong->m_fMusicSampleLengthSeconds) ); @@ -3245,20 +3433,7 @@ void ScreenEdit::HandleMainMenuChoice( MainMenuChoice c, const vector &iAns break; case edit_timing_data: { - const Song* pSong = GAMESTATE->m_pCurSong; - TimingData pTime = pSong->m_Timing; - const float fBeat = GAMESTATE->m_fSongBeat; - g_TimingDataInformation.rows[bpm].SetOneUnthemedChoice( ssprintf("%.5f", pSong->GetBPMAtBeat( fBeat ) ) ); - g_TimingDataInformation.rows[stop].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetStopAtBeat( fBeat ) ) ) ; - g_TimingDataInformation.rows[delay].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetDelayAtBeat( fBeat ) ) ); - g_TimingDataInformation.rows[time_signature_numerator].SetOneUnthemedChoice( ssprintf("%d", pTime.GetTimeSignatureNumeratorAtBeat( fBeat ) ) ); - g_TimingDataInformation.rows[time_signature_denominator].SetOneUnthemedChoice( ssprintf("%d", pTime.GetTimeSignatureDenominatorAtBeat( fBeat ) ) ); - g_TimingDataInformation.rows[tickcount].SetOneUnthemedChoice( ssprintf("%d", pTime.GetTickcountAtBeat( fBeat ) ) ); - g_TimingDataInformation.rows[combo].SetOneUnthemedChoice( ssprintf("%d", pTime.GetComboAtBeat( fBeat ) ) ); - g_TimingDataInformation.rows[label].SetOneUnthemedChoice( pTime.GetLabelAtBeat( fBeat ).c_str() ); - g_TimingDataInformation.rows[warp].SetOneUnthemedChoice( ssprintf("%.5f", pTime.GetWarpAtBeat( fBeat ) ) ); - - EditMiniMenu( &g_TimingDataInformation, SM_BackFromTimingDataInformation ); + DisplayTimingMenu(); } break; @@ -3347,12 +3522,14 @@ void ScreenEdit::HandleAreaMenuChoice( AreaMenuChoice c, const vector &iAns case paste_at_current_beat: case paste_at_begin_marker: { + // TODO: Song/Step Timing checks? + int iDestFirstRow = -1; switch( c ) { DEFAULT_FAIL( c ); case paste_at_current_beat: - iDestFirstRow = BeatToNoteRow( GAMESTATE->m_fSongBeat ); + iDestFirstRow = BeatToNoteRow( GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat ); break; case paste_at_begin_marker: ASSERT( m_NoteFieldEdit.m_iBeginMarker!=-1 ); @@ -3491,9 +3668,10 @@ void ScreenEdit::HandleAreaMenuChoice( AreaMenuChoice c, const vector &iAns NoteDataUtil::ScaleRegion( m_NoteDataEdit, fScale, iStartIndex, iEndIndex ); // scale timing data - m_pSong->m_Timing.ScaleRegion( fScale, m_NoteFieldEdit.m_iBeginMarker, m_NoteFieldEdit.m_iEndMarker, true ); + GetAppropriateTiming().ScaleRegion( fScale, m_NoteFieldEdit.m_iBeginMarker, m_NoteFieldEdit.m_iEndMarker, true ); // scale all other steps. + /* const vector sIter = m_pSong->GetAllSteps(); RString sTempStyle, sTempDiff; for( unsigned i = 0; i < sIter.size(); i++ ) @@ -3512,7 +3690,7 @@ void ScreenEdit::HandleAreaMenuChoice( AreaMenuChoice c, const vector &iAns NoteDataUtil::ScaleRegion( ndTemp, fScale, m_NoteFieldEdit.m_iBeginMarker, m_NoteFieldEdit.m_iEndMarker ); sIter[i]->SetNoteData( ndTemp ); } - + */ m_NoteFieldEdit.m_iEndMarker = iNewEndIndex; } @@ -3530,22 +3708,22 @@ void ScreenEdit::HandleAreaMenuChoice( AreaMenuChoice c, const vector &iAns TransitionEditState( STATE_RECORDING ); break; case insert_and_shift: - NoteDataUtil::InsertRows( m_NoteDataEdit, BeatToNoteRow(GAMESTATE->m_fSongBeat), BeatToNoteRow(1) ); + NoteDataUtil::InsertRows( m_NoteDataEdit, BeatToNoteRow( GetBeat() ), BeatToNoteRow(1) ); break; case delete_and_shift: - NoteDataUtil::DeleteRows( m_NoteDataEdit, BeatToNoteRow(GAMESTATE->m_fSongBeat), BeatToNoteRow(1) ); + NoteDataUtil::DeleteRows( m_NoteDataEdit, BeatToNoteRow( GetBeat() ), BeatToNoteRow(1) ); break; case shift_pauses_forward: - m_pSong->m_Timing.InsertRows( BeatToNoteRow(GAMESTATE->m_fSongBeat), BeatToNoteRow(1) ); + GetAppropriateTiming().InsertRows( BeatToNoteRow( GetBeat() ), BeatToNoteRow(1) ); break; case shift_pauses_backward: - m_pSong->m_Timing.DeleteRows( BeatToNoteRow(GAMESTATE->m_fSongBeat), BeatToNoteRow(1) ); + GetAppropriateTiming().DeleteRows( BeatToNoteRow( GetBeat() ), BeatToNoteRow(1) ); break; case convert_to_pause: { ASSERT( m_NoteFieldEdit.m_iBeginMarker!=-1 && m_NoteFieldEdit.m_iEndMarker!=-1 ); - float fMarkerStart = m_pSong->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iBeginMarker) ); - float fMarkerEnd = m_pSong->m_Timing.GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iEndMarker) ); + float fMarkerStart = GetAppropriateTiming().GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iBeginMarker) ); + float fMarkerEnd = GetAppropriateTiming().GetElapsedTimeFromBeat( NoteRowToBeat(m_NoteFieldEdit.m_iEndMarker) ); // The length of the stop segment we're going to create. This includes time spent in any // stops in the selection, which will be deleted and subsumed into the new stop. @@ -3557,9 +3735,9 @@ void ScreenEdit::HandleAreaMenuChoice( AreaMenuChoice c, const vector &iAns m_NoteFieldEdit.m_iBeginMarker + 1, m_NoteFieldEdit.m_iEndMarker-m_NoteFieldEdit.m_iBeginMarker ); - m_pSong->m_Timing.DeleteRows( m_NoteFieldEdit.m_iBeginMarker, + GetAppropriateTiming().DeleteRows( m_NoteFieldEdit.m_iBeginMarker, m_NoteFieldEdit.m_iEndMarker-m_NoteFieldEdit.m_iBeginMarker ); - m_pSong->m_Timing.SetStopAtRow( m_NoteFieldEdit.m_iBeginMarker, fStopLength ); + GetAppropriateTiming().SetStopAtRow( m_NoteFieldEdit.m_iBeginMarker, fStopLength ); m_NoteFieldEdit.m_iBeginMarker = -1; m_NoteFieldEdit.m_iEndMarker = -1; break; @@ -3567,14 +3745,14 @@ void ScreenEdit::HandleAreaMenuChoice( AreaMenuChoice c, const vector &iAns case convert_pause_to_beat: { // TODO: Convert both Delays and Stops at once. - float fStopSeconds = m_pSong->m_Timing.GetStopAtRow( BeatToNoteRow(GAMESTATE->m_fSongBeat) ); - m_pSong->m_Timing.SetStopAtBeat( GAMESTATE->m_fSongBeat, 0 ); + float fStopSeconds = GetAppropriateTiming().GetStopAtRow( BeatToNoteRow( GetBeat() ) ); + GetAppropriateTiming().SetStopAtBeat( GetBeat() , 0 ); - float fStopBeats = fStopSeconds * m_pSong->GetBPMAtBeat(GAMESTATE->m_fSongBeat) / 60; + float fStopBeats = fStopSeconds * GetAppropriateTiming().GetBPMAtBeat( GetBeat() ) / 60; // don't move the step from where it is, just move everything later - NoteDataUtil::InsertRows( m_NoteDataEdit, BeatToNoteRow(GAMESTATE->m_fSongBeat) + 1, BeatToNoteRow(fStopBeats) ); - m_pSong->m_Timing.InsertRows( BeatToNoteRow(GAMESTATE->m_fSongBeat) + 1, BeatToNoteRow(fStopBeats) ); + NoteDataUtil::InsertRows( m_NoteDataEdit, BeatToNoteRow( GetBeat() ) + 1, BeatToNoteRow(fStopBeats) ); + GetAppropriateTiming().InsertRows( BeatToNoteRow( GetBeat() ) + 1, BeatToNoteRow(fStopBeats) ); } break; case undo: @@ -3652,7 +3830,6 @@ static LocalizedString ENTER_CREDIT ("ScreenEdit","Enter a new credit."); static LocalizedString ENTER_MAIN_TITLE_TRANSLIT ("ScreenEdit","Enter a new main title transliteration."); static LocalizedString ENTER_SUB_TITLE_TRANSLIT ("ScreenEdit","Enter a new sub title transliteration."); static LocalizedString ENTER_ARTIST_TRANSLIT ("ScreenEdit","Enter a new artist transliteration."); -static LocalizedString ENTER_BEAT_0_OFFSET ("ScreenEdit","Enter the offset for the song."); static LocalizedString ENTER_LAST_BEAT_HINT ("ScreenEdit","Enter a new last beat hint."); static LocalizedString ENTER_PREVIEW_START ("ScreenEdit","Enter a new preview start."); static LocalizedString ENTER_PREVIEW_LENGTH ("ScreenEdit","Enter a new preview length."); @@ -3689,11 +3866,6 @@ void ScreenEdit::HandleSongInformationChoice( SongInformationChoice c, const vec case artist_transliteration: ScreenTextEntry::TextEntry( SM_None, ENTER_ARTIST_TRANSLIT, pSong->m_sArtistTranslit, 100, NULL, ChangeArtistTranslit, NULL ); break; - case beat_0_offset: - ScreenTextEntry::TextEntry( SM_None, ENTER_BEAT_0_OFFSET, - ssprintf("%.5f", pSong->m_Timing.m_fBeat0OffsetInSeconds), 20, - ScreenTextEntry::FloatValidate, ChangeBeat0Offset, NULL ); - break; case last_beat_hint: ScreenTextEntry::TextEntry( SM_None, ENTER_LAST_BEAT_HINT, ssprintf("%.5f", pSong->m_fSpecifiedLastBeat), 20, @@ -3722,25 +3894,36 @@ void ScreenEdit::HandleSongInformationChoice( SongInformationChoice c, const vec }; } + +static LocalizedString ENTER_BEAT_0_OFFSET ( "ScreenEdit", "Enter the offset for the song."); static LocalizedString ENTER_BPM_VALUE ( "ScreenEdit", "Enter a new BPM value." ); static LocalizedString ENTER_STOP_VALUE ( "ScreenEdit", "Enter a new Stop value." ); static LocalizedString ENTER_DELAY_VALUE ( "ScreenEdit", "Enter a new Delay value." ); -static LocalizedString ENTER_TIME_SIGNATURE_NUMERATOR_VALUE ( "ScreenEdit", "Enter a new Time Signature numerator value." ); -static LocalizedString ENTER_TIME_SIGNATURE_DENOMINATOR_VALUE ( "ScreenEdit", "Enter a new Time Signature denominator value." ); +static LocalizedString ENTER_TIME_SIGNATURE_VALUE ( "ScreenEdit", "Enter a new Time Signature." ); static LocalizedString ENTER_TICKCOUNT_VALUE ( "ScreenEdit", "Enter a new Tickcount value." ); static LocalizedString ENTER_COMBO_VALUE ( "ScreenEdit", "Enter a new Combo value." ); static LocalizedString ENTER_LABEL_VALUE ( "ScreenEdit", "Enter a new Label value." ); static LocalizedString ENTER_WARP_VALUE ( "ScreenEdit", "Enter a new Warp value." ); +static LocalizedString ENTER_SPEED_PERCENT_VALUE ( "ScreenEdit", "Enter a new Speed percent value." ); +static LocalizedString ENTER_SPEED_WAIT_VALUE ( "ScreenEdit", "Enter a new Speed wait value." ); +static LocalizedString ENTER_SPEED_MODE_VALUE ( "ScreenEdit", "Enter a new Speed mode value." ); +static LocalizedString ENTER_FAKE_VALUE ( "ScreenEdit", "Enter a new Fake value." ); +static LocalizedString CONFIRM_TIMING_ERASE ( "ScreenEdit", "Are you sure you want to erase this chart's timing data?" ); void ScreenEdit::HandleTimingDataInformationChoice( TimingDataInformationChoice c, const vector &iAnswers ) { switch( c ) { DEFAULT_FAIL( c ); - case bpm: + case beat_0_offset: + ScreenTextEntry::TextEntry( SM_None, ENTER_BEAT_0_OFFSET, + ssprintf("%.5f", GetAppropriateTiming().m_fBeat0OffsetInSeconds), 20, + ScreenTextEntry::FloatValidate, ChangeBeat0Offset, NULL ); + break; + case bpm: ScreenTextEntry::TextEntry( SM_BackFromBPMChange, ENTER_BPM_VALUE, - ssprintf( "%.4f", m_pSong->GetBPMAtBeat(GAMESTATE->m_fSongBeat) ), + ssprintf( "%.4f", GetAppropriateTiming().GetBPMAtBeat( GetBeat() ) ), 10 ); break; @@ -3748,7 +3931,7 @@ void ScreenEdit::HandleTimingDataInformationChoice( TimingDataInformationChoice ScreenTextEntry::TextEntry( SM_BackFromStopChange, ENTER_STOP_VALUE, - ssprintf( "%.4f", m_pSong->m_Timing.GetStopAtRow( BeatToNoteRow(GAMESTATE->m_fSongBeat) ) ), + ssprintf( "%.4f", GetAppropriateTiming().GetStopAtBeat( GetBeat() ) ), 10 ); break; @@ -3756,31 +3939,23 @@ void ScreenEdit::HandleTimingDataInformationChoice( TimingDataInformationChoice ScreenTextEntry::TextEntry( SM_BackFromDelayChange, ENTER_DELAY_VALUE, - ssprintf( "%.4f", m_pSong->m_Timing.GetDelayAtRow( BeatToNoteRow(GAMESTATE->m_fSongBeat) ) ), + ssprintf( "%.4f", GetAppropriateTiming().GetDelayAtBeat( GetBeat() ) ), 10 ); break; - case time_signature_numerator: + case time_signature: ScreenTextEntry::TextEntry( - SM_BackFromTimeSignatureNumeratorChange, - ENTER_TIME_SIGNATURE_NUMERATOR_VALUE, - ssprintf( "%d", m_pSong->m_Timing.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_fSongBeat ).m_iNumerator ), - 3 - ); - break; - case time_signature_denominator: - ScreenTextEntry::TextEntry( - SM_BackFromTimeSignatureDenominatorChange, - ENTER_TIME_SIGNATURE_DENOMINATOR_VALUE, - ssprintf( "%d", m_pSong->m_Timing.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_fSongBeat ).m_iDenominator ), - 3 + SM_BackFromTimeSignatureChange, + ENTER_TIME_SIGNATURE_VALUE, + ssprintf( "%d/%d", GetAppropriateTiming().GetTimeSignatureSegmentAtBeat( GetBeat() ).m_iNumerator, GetAppropriateTiming().GetTimeSignatureSegmentAtBeat( GetBeat() ).m_iDenominator ), + 8 ); break; case tickcount: ScreenTextEntry::TextEntry( SM_BackFromTickcountChange, ENTER_TICKCOUNT_VALUE, - ssprintf( "%d", m_pSong->m_Timing.GetTickcountAtBeat( GAMESTATE->m_fSongBeat ) ), + ssprintf( "%d", GetAppropriateTiming().GetTickcountAtBeat( GetBeat() ) ), 2 ); break; @@ -3788,7 +3963,7 @@ void ScreenEdit::HandleTimingDataInformationChoice( TimingDataInformationChoice ScreenTextEntry::TextEntry( SM_BackFromComboChange, ENTER_COMBO_VALUE, - ssprintf( "%d", m_pSong->m_Timing.GetComboAtBeat( GAMESTATE->m_fSongBeat ) ), + ssprintf( "%d", GetAppropriateTiming().GetComboAtBeat( GetBeat() ) ), 4 ); break; @@ -3796,7 +3971,7 @@ void ScreenEdit::HandleTimingDataInformationChoice( TimingDataInformationChoice ScreenTextEntry::TextEntry( SM_BackFromLabelChange, ENTER_LABEL_VALUE, - ssprintf( "%s", m_pSong->m_Timing.GetLabelAtBeat( GAMESTATE->m_fSongBeat ).c_str() ), + ssprintf( "%s", GetAppropriateTiming().GetLabelAtBeat( GetBeat() ).c_str() ), 64 ); break; @@ -3804,10 +3979,51 @@ void ScreenEdit::HandleTimingDataInformationChoice( TimingDataInformationChoice ScreenTextEntry::TextEntry( SM_BackFromWarpChange, ENTER_WARP_VALUE, - ssprintf( "%.4f", m_pSong->m_Timing.GetWarpAtRow( BeatToNoteRow(GAMESTATE->m_fSongBeat) ) ), + ssprintf( "%.4f", GetAppropriateTiming().GetWarpAtBeat( GetBeat() ) ), 10 ); + break; + case speed_percent: + ScreenTextEntry::TextEntry( + SM_BackFromSpeedPercentChange, + ENTER_SPEED_PERCENT_VALUE, + ssprintf( "%.5f", GetAppropriateTiming().GetSpeedSegmentAtBeat( GetBeat() ).m_fPercent ), + 10 + ); + break; + case speed_wait: + ScreenTextEntry::TextEntry( + SM_BackFromSpeedWaitChange, + ENTER_SPEED_WAIT_VALUE, + ssprintf( "%.5f", GetAppropriateTiming().GetSpeedSegmentAtBeat( GetBeat() ).m_fWait ), + 10 + ); + break; + case speed_mode: + { + ScreenTextEntry::TextEntry( + SM_BackFromSpeedModeChange, + ENTER_SPEED_MODE_VALUE, + "", + 3 + ); + + break; + } + case fake: + { + ScreenTextEntry::TextEntry( + SM_BackFromFakeChange, + ENTER_FAKE_VALUE, + ssprintf("%.5f", GetAppropriateTiming().GetFakeAtBeat( GetBeat() ) ), + 10 + ); + break; + } + case erase_step_timing: + ScreenPrompt::Prompt( SM_DoEraseStepTiming, CONFIRM_TIMING_ERASE , PROMPT_YES_NO, ANSWER_NO ); break; + } } @@ -3817,7 +4033,7 @@ void ScreenEdit::HandleBGChangeChoice( BGChangeChoice c, const vector &iAns FOREACH( BackgroundChange, m_pSong->GetBackgroundChanges(g_CurrentBGChangeLayer), iter ) { - if( iter->m_fStartBeat == GAMESTATE->m_fSongBeat ) + if( iter->m_fStartBeat == GAMESTATE->m_Position.m_fSongBeat ) { newChange = *iter; // delete the old change. We'll add a new one below. @@ -3826,7 +4042,7 @@ void ScreenEdit::HandleBGChangeChoice( BGChangeChoice c, const vector &iAns } } - newChange.m_fStartBeat = GAMESTATE->m_fSongBeat; + newChange.m_fStartBeat = GAMESTATE->m_Position.m_fSongBeat; newChange.m_fRate = StringToFloat( g_BackgroundChange.rows[rate].choices[iAnswers[rate]] )/100.f; newChange.m_sTransition = iAnswers[transition] ? g_BackgroundChange.rows[transition].choices[iAnswers[transition]] : RString(); newChange.m_def.m_sEffect = iAnswers[effect] ? g_BackgroundChange.rows[effect].choices[iAnswers[effect]] : RString(); @@ -3961,7 +4177,7 @@ void ScreenEdit::RevertFromDisk() // put a blank Steps in its place. Note that this does not have to be the // work of someone maliciously changing the simfile; it could happen to // someone editing a new stepchart and reverting from disk, for example. - pNewSteps = new Steps(); + pNewSteps = GAMESTATE->m_pCurSong->CreateSteps(); pNewSteps->CreateBlank( id.GetStepsType() ); pNewSteps->SetDifficulty( id.GetDifficulty() ); GAMESTATE->m_pCurSong->AddSteps( pNewSteps ); @@ -4011,7 +4227,7 @@ void ScreenEdit::CheckNumberOfNotesAndUndo() if( EDIT_MODE.GetValue() != EditMode_Home ) return; - TimeSignatureSegment curTime = GAMESTATE->m_pCurSong->m_Timing.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_fSongBeat ); + TimeSignatureSegment curTime = GAMESTATE->m_pCurSong->m_SongTiming.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat ); int rowsPerMeasure = curTime.m_iDenominator * curTime.m_iNumerator; for( int row=0; row<=m_NoteDataEdit.GetLastRow(); row+=rowsPerMeasure ) @@ -4063,7 +4279,7 @@ float ScreenEdit::GetMaximumBeatForNewNote() const /* Round up to the next measure end. Some songs end on weird beats * mid-measure, and it's odd to have movement capped to these weird * beats. */ - int beatsPerMeasure = GAMESTATE->m_pCurSong->m_Timing.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_fSongBeat ).m_iNumerator; + int beatsPerMeasure = GAMESTATE->m_pCurSong->m_SongTiming.GetTimeSignatureSegmentAtBeat( GAMESTATE->m_pPlayerState[PLAYER_1]->m_Position.m_fSongBeat ).m_iNumerator; fEndBeat += beatsPerMeasure; fEndBeat = ftruncf( fEndBeat, (float)beatsPerMeasure ); @@ -4120,6 +4336,7 @@ struct EditHelpLine #undef PUSH_IF_VALID } }; +// TODO: Identify which of these can be removed and sent to a readme. static const EditHelpLine g_EditHelpLines[] = { EditHelpLine( "Move cursor", EDIT_BUTTON_SCROLL_UP_LINE, EDIT_BUTTON_SCROLL_DOWN_LINE ), @@ -4148,6 +4365,7 @@ static const EditHelpLine g_EditHelpLines[] = EditHelpLine( "Shift BPM changes and stops up one beat", EDIT_BUTTON_DELETE_SHIFT_PAUSES ), EditHelpLine( "Cycle between tap notes", EDIT_BUTTON_CYCLE_TAP_LEFT, EDIT_BUTTON_CYCLE_TAP_RIGHT ), EditHelpLine( "Add to/remove from right half", EDIT_BUTTON_RIGHT_SIDE ), + EditHelpLine( "Switch Timing", EDIT_BUTTON_SWITCH_TIMINGS ), EditHelpLine( "Switch player (Routine only)", EDIT_BUTTON_SWITCH_PLAYERS ), }; diff --git a/src/ScreenEdit.h b/src/ScreenEdit.h index 4de415230b..d4ac2af111 100644 --- a/src/ScreenEdit.h +++ b/src/ScreenEdit.h @@ -86,6 +86,7 @@ enum EditButton EDIT_BUTTON_SNAP_PREV, EDIT_BUTTON_OPEN_EDIT_MENU, + EDIT_BUTTON_OPEN_TIMING_MENU, EDIT_BUTTON_OPEN_AREA_MENU, EDIT_BUTTON_OPEN_BGCHANGE_LAYER1_MENU, EDIT_BUTTON_OPEN_BGCHANGE_LAYER2_MENU, @@ -137,6 +138,8 @@ enum EditButton EDIT_BUTTON_ADD_COURSE_MODS, EDIT_BUTTON_SWITCH_PLAYERS, /**< Allow entering notes for a different Player. */ + + EDIT_BUTTON_SWITCH_TIMINGS, /**< Allow switching between Song and Step TimingData. */ NUM_EditButton, // leave this at the end EditButton_Invalid @@ -231,6 +234,9 @@ protected: float GetMaximumBeatForMoving() const; // don't allow Down key to go past this beat. void DoHelp(); + + /** @brief Display the TimingData menu for editing song and step timing. */ + void DisplayTimingMenu(); EditState m_EditState; @@ -275,14 +281,22 @@ protected: /** @brief Has the NoteData been changed such that a user should be prompted to save? */ bool m_bDirty; + /** @brief The sound that is played when a note is added. */ RageSound m_soundAddNote; + /** @brief The sound that is played when a note is removed. */ RageSound m_soundRemoveNote; RageSound m_soundChangeLine; RageSound m_soundChangeSnap; RageSound m_soundMarker; RageSound m_soundValueIncrease; RageSound m_soundValueDecrease; + /** @brief The sound that is played when switching players for Routine. */ + RageSound m_soundSwitchPlayer; + /** @brief The sound that is played when switching song/step timing. */ + RageSound m_soundSwitchTiming; + /** @brief The sound that is played when switching to a different chart. */ RageSound m_soundSwitchSteps; + /** @brief The sound that is played when the chart is saved. */ RageSound m_soundSave; // used for reverting @@ -459,7 +473,6 @@ public: main_title_transliteration, sub_title_transliteration, artist_transliteration, - beat_0_offset, last_beat_hint, preview_start, preview_length, @@ -472,16 +485,21 @@ public: enum TimingDataInformationChoice { + beat_0_offset, bpm, stop, delay, -// time_signature, - time_signature_numerator, - time_signature_denominator, + time_signature, + label, tickcount, combo, - label, warp, +// speed, + speed_percent, + speed_wait, + speed_mode, + fake, + erase_step_timing, NUM_TIMING_DATA_INFORMATION_CHOICES }; @@ -514,7 +532,19 @@ public: delete_change, NUM_BGCHANGE_CHOICES }; + + enum SpeedSegmentModes + { + SSMODE_Beats, + SSMODE_Seconds + }; + /** + * @brief Take care of any background changes that the user wants. + * + * It is important that this is only called in Song Timing mode. + * @param c the Background Change style requested. + * @param iAnswers the other settings involving the change. */ void HandleBGChangeChoice( BGChangeChoice c, const vector &iAnswers ); enum CourseAttackChoice @@ -544,6 +574,14 @@ public: void MakeFilteredMenuDef( const MenuDef* pDef, MenuDef &menu ); void EditMiniMenu( const MenuDef* pDef, ScreenMessage SM_SendOnOK = SM_None, ScreenMessage SM_SendOnCancel = SM_None ); +private: + /** + * @brief Retrieve the appropriate TimingData based on GAMESTATE. + * @return the proper TimingData. */ + TimingData & GetAppropriateTiming() const; + void SetBeat(float fBeat); + float GetBeat(); + }; #endif diff --git a/src/ScreenEditMenu.cpp b/src/ScreenEditMenu.cpp index 04bb81e213..1bf4997aea 100644 --- a/src/ScreenEditMenu.cpp +++ b/src/ScreenEditMenu.cpp @@ -236,7 +236,7 @@ void ScreenEditMenu::MenuStart( const InputEventPlus &input ) case EditMenuAction_Create: ASSERT( !pSteps ); { - pSteps = new Steps; + pSteps = pSong->CreateSteps(); switch( m_Selector.EDIT_MODE ) { diff --git a/src/ScreenGameplay.cpp b/src/ScreenGameplay.cpp index b3dbc5a791..231047f933 100644 --- a/src/ScreenGameplay.cpp +++ b/src/ScreenGameplay.cpp @@ -1325,7 +1325,7 @@ void ScreenGameplay::StartPlayingSong( float fMinTimeToNotes, float fMinTimeToMu { const float fFirstBeat = GAMESTATE->m_pCurSong->m_fFirstBeat; - const float fFirstSecond = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat( fFirstBeat ); + const float fFirstSecond = GAMESTATE->m_pCurSong->m_SongTiming.GetElapsedTimeFromBeat( fFirstBeat ); float fStartDelay = fMinTimeToNotes - fFirstSecond; fStartDelay = max( fStartDelay, fMinTimeToMusic ); p.m_StartSecond = -fStartDelay; @@ -1347,10 +1347,10 @@ void ScreenGameplay::StartPlayingSong( float fMinTimeToNotes, float fMinTimeToMu m_pSoundMusic->Pause( true ); /* Make sure GAMESTATE->m_fMusicSeconds is set up. */ - GAMESTATE->m_fMusicSeconds = -5000; + GAMESTATE->m_Position.m_fMusicSeconds = -5000; UpdateSongPosition(0); - ASSERT( GAMESTATE->m_fMusicSeconds > -4000 ); /* make sure the "fake timer" code doesn't trigger */ + ASSERT( GAMESTATE->m_Position.m_fMusicSeconds > -4000 ); /* make sure the "fake timer" code doesn't trigger */ } @@ -1405,7 +1405,7 @@ void ScreenGameplay::PlayAnnouncer( RString type, float fSeconds ) if( m_DancingState != STATE_DANCING ) return; if( GAMESTATE->m_pCurSong == NULL || // this will be true on ScreenDemonstration sometimes - GAMESTATE->m_fSongBeat < GAMESTATE->m_pCurSong->m_fFirstBeat ) + GAMESTATE->m_Position.m_fSongBeat < GAMESTATE->m_pCurSong->m_fFirstBeat ) return; if( m_fTimeSinceLastDancingComment < fSeconds ) @@ -1423,7 +1423,7 @@ void ScreenGameplay::UpdateSongPosition( float fDeltaTime ) RageTimer tm; const float fSeconds = m_pSoundMusic->GetPositionSeconds( NULL, &tm ); const float fAdjust = SOUND->GetFrameTimingAdjustment( fDeltaTime ); - GAMESTATE->UpdateSongPosition( fSeconds+fAdjust, GAMESTATE->m_pCurSong->m_Timing, tm+fAdjust ); + GAMESTATE->UpdateSongPosition( fSeconds+fAdjust, GAMESTATE->m_pCurSong->m_SongTiming, tm+fAdjust, true ); } void ScreenGameplay::BeginScreen() @@ -1480,7 +1480,7 @@ bool ScreenGameplay::AllAreFailing() void ScreenGameplay::GetMusicEndTiming( float &fSecondsToStartFadingOutMusic, float &fSecondsToStartTransitioningOut ) { - float fLastStepSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat( GAMESTATE->m_pCurSong->m_fLastBeat ); + float fLastStepSeconds = GAMESTATE->m_pCurSong->m_SongTiming.GetElapsedTimeFromBeat( GAMESTATE->m_pCurSong->m_fLastBeat ); fLastStepSeconds += Player::GetMaxStepDistanceSeconds(); float fTransitionLength; @@ -1687,7 +1687,7 @@ void ScreenGameplay::Update( float fDeltaTime ) // update fGameplaySeconds STATSMAN->m_CurStageStats.m_fGameplaySeconds += fUnscaledDeltaTime; - if( GAMESTATE->m_fSongBeat >= GAMESTATE->m_pCurSong->m_fFirstBeat && GAMESTATE->m_fSongBeat < GAMESTATE->m_pCurSong->m_fLastBeat ) + if( GAMESTATE->m_Position.m_fSongBeat >= GAMESTATE->m_pCurSong->m_fFirstBeat && GAMESTATE->m_Position.m_fSongBeat < GAMESTATE->m_pCurSong->m_fLastBeat ) { STATSMAN->m_CurStageStats.m_fStepsSeconds += fUnscaledDeltaTime; @@ -1707,7 +1707,7 @@ void ScreenGameplay::Update( float fDeltaTime ) if( bAllReallyFailed ) fSecondsToStartTransitioningOut += BEGIN_FAILED_DELAY; - if( GAMESTATE->m_fMusicSeconds >= fSecondsToStartTransitioningOut && !m_NextSong.IsTransitioning() ) + if( GAMESTATE->m_Position.m_fMusicSeconds >= fSecondsToStartTransitioningOut && !m_NextSong.IsTransitioning() ) this->PostScreenMessage( SM_NotesEnded, 0 ); } @@ -1843,8 +1843,8 @@ void ScreenGameplay::Update( float fDeltaTime ) float ScreenGameplay::GetHasteRate() { - if( GAMESTATE->m_fMusicSeconds < GAMESTATE->m_fLastHasteUpdateMusicSeconds || // new song - GAMESTATE->m_fMusicSeconds > GAMESTATE->m_fLastHasteUpdateMusicSeconds + 4 ) + 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 ) @@ -1865,7 +1865,7 @@ float ScreenGameplay::GetHasteRate() GAMESTATE->m_fHasteRate += 0.1f; CLAMP( GAMESTATE->m_fHasteRate, -1.0f, +1.0f ); - GAMESTATE->m_fLastHasteUpdateMusicSeconds = GAMESTATE->m_fMusicSeconds; + GAMESTATE->m_fLastHasteUpdateMusicSeconds = GAMESTATE->m_Position.m_fMusicSeconds; } /* If the life meter is less than half full, push the haste rate down to let @@ -1914,7 +1914,7 @@ void ScreenGameplay::UpdateLights() ZERO( bBlinkGameButton ); bool bCrossedABeat = false; { - const float fSongBeat = GAMESTATE->m_fLightSongBeat; + const float fSongBeat = GAMESTATE->m_Position.m_fLightSongBeat; const int iSongRow = BeatToNoteRowNotRounded( fSongBeat ); static int iRowLastCrossed = 0; @@ -1968,7 +1968,7 @@ void ScreenGameplay::UpdateLights() } // Before the first beat of the song, all cabinet lights solid on (except for menu buttons). - bool bOverrideCabinetBlink = (GAMESTATE->m_fSongBeat < GAMESTATE->m_pCurSong->m_fFirstBeat); + bool bOverrideCabinetBlink = (GAMESTATE->m_Position.m_fSongBeat < GAMESTATE->m_pCurSong->m_fFirstBeat); FOREACH_CabinetLight( cl ) bBlinkCabinetLight[cl] |= bOverrideCabinetBlink; @@ -1994,8 +1994,8 @@ void ScreenGameplay::SendCrossedMessages() { static int iRowLastCrossed = 0; - float fPositionSeconds = GAMESTATE->m_fMusicSeconds; - float fSongBeat = GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ); + float fPositionSeconds = GAMESTATE->m_Position.m_fMusicSeconds; + float fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds ); int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); iRowNow = max( 0, iRowNow ); @@ -2032,8 +2032,8 @@ void ScreenGameplay::SendCrossedMessages() { float fNoteWillCrossInSeconds = MESSAGE_SPACING_SECONDS * i; - float fPositionSeconds = GAMESTATE->m_fMusicSeconds + fNoteWillCrossInSeconds; - float fSongBeat = GAMESTATE->m_pCurSong->GetBeatFromElapsedTime( fPositionSeconds ); + float fPositionSeconds = GAMESTATE->m_Position.m_fMusicSeconds + fNoteWillCrossInSeconds; + float fSongBeat = GAMESTATE->m_pCurSong->m_SongTiming.GetBeatFromElapsedTime( fPositionSeconds ); int iRowNow = BeatToNoteRowNotRounded( fSongBeat ); iRowNow = max( 0, iRowNow ); diff --git a/src/ScreenHowToPlay.cpp b/src/ScreenHowToPlay.cpp index 747a0bf18b..81e8d73d6b 100644 --- a/src/ScreenHowToPlay.cpp +++ b/src/ScreenHowToPlay.cpp @@ -208,7 +208,7 @@ void ScreenHowToPlay::Step() #define ST_JUMPUD (ST_UP | ST_DOWN) int iStep = 0; - const int iNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat + 0.6f ); + 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 ) ) { @@ -229,10 +229,10 @@ void ScreenHowToPlay::Step() m_pmCharacter->PlayAnimation( "Step-JUMPLR", 1.8f ); m_pmCharacter->StopTweening(); - m_pmCharacter->BeginTweening( GAMESTATE->m_fCurBPS /8, TWEEN_LINEAR ); + m_pmCharacter->BeginTweening( GAMESTATE->m_Position.m_fCurBPS /8, TWEEN_LINEAR ); m_pmCharacter->SetRotationY( 90 ); - m_pmCharacter->BeginTweening( (1/(GAMESTATE->m_fCurBPS * 2) ) ); //sleep between jump-frames - m_pmCharacter->BeginTweening( GAMESTATE->m_fCurBPS /6, TWEEN_LINEAR ); + 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; } @@ -243,11 +243,11 @@ void ScreenHowToPlay::Update( float fDelta ) { if( GAMESTATE->m_pCurSong != NULL ) { - GAMESTATE->UpdateSongPosition( m_fFakeSecondsIntoSong, GAMESTATE->m_pCurSong->m_Timing ); + GAMESTATE->UpdateSongPosition( m_fFakeSecondsIntoSong, GAMESTATE->m_pCurSong->m_SongTiming ); m_fFakeSecondsIntoSong += fDelta; static int iLastNoteRowCounted = 0; - int iCurNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_fSongBeat ); + int iCurNoteRow = BeatToNoteRowNotRounded( GAMESTATE->m_Position.m_fSongBeat ); if( iCurNoteRow != iLastNoteRowCounted &&m_NoteData.IsThereATapAtRow(iCurNoteRow) ) { diff --git a/src/ScreenNameEntry.cpp b/src/ScreenNameEntry.cpp index fca92a59b1..b4eeeed4cc 100644 --- a/src/ScreenNameEntry.cpp +++ b/src/ScreenNameEntry.cpp @@ -61,7 +61,7 @@ void ScreenNameEntry::ScrollingText::Init( const RString &sName, const vectorm_fSongBeat; + const float fFakeBeat = GAMESTATE->m_Position.m_fSongBeat; const size_t iClosestIndex = lrintf( fFakeBeat ) % CHARS_CHOICES.size(); const float fClosestYOffset = GetClosestCharYOffset( fFakeBeat ); @@ -325,7 +325,7 @@ void ScreenNameEntry::Update( float fDelta ) SOUND->PlayOnceFromDir( ANNOUNCER->GetPathTo("name entry") ); m_fFakeBeat += fDelta * FAKE_BEATS_PER_SEC; - GAMESTATE->m_fSongBeat = m_fFakeBeat; + GAMESTATE->m_Position.m_fSongBeat = m_fFakeBeat; ScreenWithMenuElements::Update(fDelta); } diff --git a/src/ScreenSelectMusic.cpp b/src/ScreenSelectMusic.cpp index eb3acff4d9..1ee3c116a4 100644 --- a/src/ScreenSelectMusic.cpp +++ b/src/ScreenSelectMusic.cpp @@ -1764,7 +1764,7 @@ void ScreenSelectMusic::AfterMusicChange() case SampleMusicPreviewMode_LastSong: // fall through // play the sample music m_sSampleMusicToPlay = pSong->GetMusicPath(); - m_pSampleMusicTimingData = &pSong->m_Timing; + m_pSampleMusicTimingData = &pSong->m_SongTiming; m_fSampleStartSeconds = pSong->m_fMusicSampleStartSeconds; m_fSampleLengthSeconds = pSong->m_fMusicSampleLengthSeconds; break; diff --git a/src/ScreenSyncOverlay.cpp b/src/ScreenSyncOverlay.cpp index a0038b083f..5f5d8e37f4 100644 --- a/src/ScreenSyncOverlay.cpp +++ b/src/ScreenSyncOverlay.cpp @@ -221,7 +221,7 @@ bool ScreenSyncOverlay::OverlayInput( const InputEventPlus &input ) } if( GAMESTATE->m_pCurSong != NULL ) { - BPMSegment& seg = GAMESTATE->m_pCurSong->GetBPMSegmentAtBeat( GAMESTATE->m_fSongBeat ); + BPMSegment& seg = GAMESTATE->m_pCurSong->m_SongTiming.GetBPMSegmentAtBeat( GAMESTATE->m_Position.m_fSongBeat ); seg.m_fBPS += fDelta; } } @@ -253,7 +253,7 @@ bool ScreenSyncOverlay::OverlayInput( const InputEventPlus &input ) case ChangeSongOffset: if( GAMESTATE->m_pCurSong != NULL ) - GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds += fDelta; + GAMESTATE->m_pCurSong->m_SongTiming.m_fBeat0OffsetInSeconds += fDelta; break; } } diff --git a/src/Song.cpp b/src/Song.cpp index 2bc3df3d64..cca7a65b38 100644 --- a/src/Song.cpp +++ b/src/Song.cpp @@ -41,7 +41,7 @@ * @brief The internal version of the cache for StepMania. * * Increment this value to invalidate the current cache. */ -const int FILE_CACHE_VERSION = 171; +const int FILE_CACHE_VERSION = 174; /** @brief How long does a song sample last by default? */ const float DEFAULT_MUSIC_SAMPLE_LENGTH = 12.f; @@ -145,6 +145,18 @@ 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 = m_SongTiming; +} + void Song::GetDisplayBpms( DisplayBpms &AddTo ) const { if( m_DisplayBPMType == DISPLAY_BPM_SPECIFIED ) @@ -155,7 +167,7 @@ void Song::GetDisplayBpms( DisplayBpms &AddTo ) const else { float fMinBPM, fMaxBPM; - m_Timing.GetActualBPM( fMinBPM, fMaxBPM ); + m_SongTiming.GetActualBPM( fMinBPM, fMaxBPM ); AddTo.Add( fMinBPM ); AddTo.Add( fMaxBPM ); } @@ -460,6 +472,13 @@ void Song::TidyUpData() 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. */ ReCalculateRadarValuesAndLastBeat(); @@ -476,29 +495,18 @@ void Song::TidyUpData() m_sArtist = "Unknown artist"; TranslateTitles(); - if( m_Timing.m_BPMSegments.empty() ) - { - LOG->UserLog( "Song file", m_sSongDir + m_sSongFileName, "has no BPM segments, default provided." ); - - m_Timing.AddBPMSegment( BPMSegment(0, 60) ); - } - - // Make sure the first BPM segment starts at beat 0. - if( m_Timing.m_BPMSegments[0].m_iStartRow != 0 ) - m_Timing.m_BPMSegments[0].m_iStartRow = 0; - - if( m_fMusicSampleStartSeconds == -1 || m_fMusicSampleStartSeconds == 0 || m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) { - m_fMusicSampleStartSeconds = this->GetElapsedTimeFromBeat( 100 ); + const TimingData &timing = this->m_SongTiming; + m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( 100 ); if( m_fMusicSampleStartSeconds+m_fMusicSampleLengthSeconds > this->m_fMusicLengthSeconds ) { int iBeat = lrintf( m_fLastBeat/2 ); iBeat -= iBeat%4; - m_fMusicSampleStartSeconds = this->GetElapsedTimeFromBeat( (float)iBeat ); + m_fMusicSampleStartSeconds = timing.GetElapsedTimeFromBeat( (float)iBeat ); } } @@ -777,38 +785,6 @@ void Song::TidyUpData() m_sSongFileName += ".ssc"; } while(0); } - - // If no time signature specified, assume 4/4 time for the whole song. - if( m_Timing.m_vTimeSignatureSegments.empty() ) - { - TimeSignatureSegment seg(0, 4, 4); - m_Timing.m_vTimeSignatureSegments.push_back( seg ); - } - - /* - * Likewise, if no tickcount signature is specified, assume 2 ticks - * per beat for the entire song. The default of 2 is chosen more - * for compatibility with the Pump Pro series than anything else. - */ - if( m_Timing.m_TickcountSegments.empty() ) - { - TickcountSegment seg(0, 2); - m_Timing.m_TickcountSegments.push_back( seg ); - } - - // Have a default combo segment of one just in case. - if( m_Timing.m_ComboSegments.empty() ) - { - ComboSegment seg(0, 1); - m_Timing.m_ComboSegments.push_back( seg ); - } - - // Have a default label segment just in case. - if( m_Timing.m_LabelSegments.empty() ) - { - LabelSegment seg(0, "Song Start"); - m_Timing.m_LabelSegments.push_back( seg ); - } } void Song::TranslateTitles() @@ -857,8 +833,8 @@ void Song::ReCalculateRadarValuesAndLastBeat() if( tempNoteData.GetLastRow() == 0 ) continue; - fFirstBeat = min( fFirstBeat, tempNoteData.GetFirstBeat() ); - fLastBeat = max( fLastBeat, tempNoteData.GetLastBeat() ); + fFirstBeat = min( fFirstBeat, m_SongTiming.GetBeatFromElapsedTime(pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetFirstBeat())) ); + fLastBeat = max( fLastBeat, m_SongTiming.GetBeatFromElapsedTime(pSteps->m_Timing.GetElapsedTimeFromBeat(tempNoteData.GetLastBeat())) ); } m_fFirstBeat = fFirstBeat; @@ -887,6 +863,12 @@ void Song::Save() ReCalculateRadarValuesAndLastBeat(); TranslateTitles(); + + // TODO: Figure out a better way to save to Song's timing data. + if( m_vpSteps.size() == 1 ) + { + m_SongTiming = m_vpSteps[0]->m_Timing; + } // Save the new files. These calls make backups on their own. if( !SaveToSSCFile(GetSongFilePath(), false) ) @@ -1453,7 +1435,7 @@ bool Song::IsEditAlreadyLoaded( Steps* pSteps ) const bool Song::HasSignificantBpmChangesOrStops() const { - if( m_Timing.HasStops() ) + if( m_SongTiming.HasStops() ) return true; // Don't consider BPM changes that only are only for maintaining sync as @@ -1463,7 +1445,7 @@ bool Song::HasSignificantBpmChangesOrStops() const if( m_fSpecifiedBPMMin != m_fSpecifiedBPMMax ) return true; } - else if( m_Timing.HasBpmChanges() ) + else if( m_SongTiming.HasBpmChanges() ) { return true; } @@ -1473,7 +1455,8 @@ bool Song::HasSignificantBpmChangesOrStops() const float Song::GetStepsSeconds() const { - return GetElapsedTimeFromBeat( m_fLastBeat ) - GetElapsedTimeFromBeat( m_fFirstBeat ); + const TimingData &timing = this->m_SongTiming; + return timing.GetElapsedTimeFromBeat( m_fLastBeat ) - timing.GetElapsedTimeFromBeat( m_fFirstBeat ); } bool Song::IsLong() const @@ -1556,7 +1539,7 @@ public: } static int GetTimingData( T* p, lua_State *L ) { - p->m_Timing.PushSelf(L); + p->m_SongTiming.PushSelf(L); return 1; } // has functions diff --git a/src/Song.h b/src/Song.h index 479457ec56..8d3134b0f2 100644 --- a/src/Song.h +++ b/src/Song.h @@ -8,16 +8,16 @@ #include "RageUtil_AutoPtr.h" #include "RageUtil_CachedObject.h" #include "RageTypes.h" +#include "Steps.h" #include -class Steps; class Style; class StepsID; struct lua_State; struct BackgroundChange; /** @brief The version of the .ssc file format. */ -const static float STEPFILE_VERSION_NUMBER = 0.59f; +const static float STEPFILE_VERSION_NUMBER = 0.7f; /** @brief How many edits for this song can each profile have? */ const int MAX_EDITS_PER_SONG_PER_PROFILE = 5; @@ -285,7 +285,10 @@ public: bool Matches(RString sGroup, RString sSong) const; /** @brief The Song's TimingData. */ - TimingData m_Timing; + TimingData m_SongTiming; + + /** @brief The initial offset of a song. */ + float m_fBeat0OffsetInSeconds; typedef vector VBackgroundChange; private: @@ -317,9 +320,11 @@ public: * 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 ); @@ -327,18 +332,47 @@ public: 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); + /** * @brief Retrieve the beat based on the specified time. * @param fElapsedTime the amount of time since the Song started. * @return the appropriate beat. */ + /* [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; diff --git a/src/SongPosition.cpp b/src/SongPosition.cpp new file mode 100644 index 0000000000..5aa148e552 --- /dev/null +++ b/src/SongPosition.cpp @@ -0,0 +1,135 @@ +#include "global.h" +#include "SongPosition.h" + +static Preference g_fVisualDelaySeconds( "VisualDelaySeconds", 0.0f ); + +void SongPosition::UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp ) +{ + + if( !timestamp.IsZero() ) + m_LastBeatUpdate = timestamp; + else + m_LastBeatUpdate.Touch(); + + // xxx testing: only do this on monotune survivor + /* + if( m_pCurSong && m_pCurSong->GetDisplayFullTitle() == "monotune survivor" ) + LOG->Trace( ssprintf("[GameState::UpdateSongPosition] cur BPS = %f, fPositionSeconds = %f",m_fCurBPS,fPositionSeconds) ); + */ + + timing.GetBeatAndBPSFromElapsedTime( fPositionSeconds, m_fSongBeat, m_fCurBPS, m_bFreeze, m_bDelay, m_iWarpBeginRow, m_fWarpDestination ); + + // "Crash reason : -243478.890625 -48695.773438" + ASSERT_M( m_fSongBeat > -2000, ssprintf("Song beat %f at %f seconds is less than -2000!", m_fSongBeat, fPositionSeconds) ); + + m_fMusicSeconds = fPositionSeconds; + + m_fLightSongBeat = timing.GetBeatFromElapsedTime( fPositionSeconds + g_fLightsAheadSeconds ); + + m_fSongBeatNoOffset = timing.GetBeatFromElapsedTimeNoOffset( fPositionSeconds ); + + /* + // xxx testing: only do this on monotune survivor + if( m_pCurSong && m_pCurSong->GetDisplayFullTitle() == "monotune survivor" ) + { + // and only do it in the known negative bpm region. HACKITY HACK + if(m_fSongBeat >= 445.490f && m_fSongBeat <= 453.72f) + { + LOG->Trace( ssprintf("fPositionSeconds = %f",fPositionSeconds) ); + LOG->Trace( ssprintf("Song beat: %f (%f seconds), BPS = %f (%f BPM)",m_fSongBeat,m_fMusicSecondsVisible,m_fCurBPS,m_fCurBPS*60.0f) ); + //LOG->Trace( ssprintf("Music seconds visible %f = fPositionSeconds %f - g_fVisualDelaySeconds %f", m_fMusicSecondsVisible,fPositionSeconds,g_fVisualDelaySeconds.Get()) ); + } + else if(m_fSongBeat == 445.500f) + { + LOG->Trace( ssprintf("[beat 445.500] fPositionSeconds = %f",fPositionSeconds) ); + LOG->Trace( ssprintf("Song beat: %f (%f seconds), BPS = %f (%f BPM)",m_fSongBeat,m_fMusicSecondsVisible,m_fCurBPS,m_fCurBPS*60.0f) ); + } + } + */ + + m_fMusicSecondsVisible = fPositionSeconds - g_fVisualDelaySeconds.Get(); + float fThrowAway, fThrowAway2; + bool bThrowAway; + int iThrowAway; + timing.GetBeatAndBPSFromElapsedTime( m_fMusicSecondsVisible, m_fSongBeatVisible, fThrowAway, bThrowAway, bThrowAway, iThrowAway, fThrowAway2 ); + +} + +void SongPosition::Reset() +{ + + m_fMusicSecondsVisible = 0; + m_fSongBeatVisible = 0; + + m_fMusicSeconds = 0; // MUSIC_SECONDS_INVALID; + // todo: move me to FOREACH_EnabledPlayer( p ) after [NUM_PLAYERS]ing + m_fSongBeat = 0; + m_fSongBeatNoOffset = 0; + m_fCurBPS = 10; + //m_bStop = false; + m_bFreeze = false; + m_bDelay = false; + m_iWarpBeginRow = -1; // Set to -1 because some song may want to warp to row 0. -aj + m_fWarpDestination = -1; // Set when a warp is encountered. also see above. -aj + +} + +//lua start +#include "LuaBinding.h" +class LunaSongPosition: public Luna +{ +public: + DEFINE_METHOD( GetMusicSecondsVisible, m_fMusicSecondsVisible ); + DEFINE_METHOD( GetSongBeatVisible, m_fSongBeatVisible ); + DEFINE_METHOD( GetMusicSeconds, m_fMusicSeconds ); + DEFINE_METHOD( GetSongBeat, m_fSongBeat ); + DEFINE_METHOD( GetSongBeatNoOffset, m_fSongBeatNoOffset ); + DEFINE_METHOD( GetCurBPS, m_fCurBPS ); + DEFINE_METHOD( GetFreeze, m_bFreeze ); + DEFINE_METHOD( GetDelay, m_bDelay ); + DEFINE_METHOD( GetWarpBeginRow, m_iWarpBeginRow ); + DEFINE_METHOD( GetWarpDestination, m_fWarpDestination ); + + LunaSongPosition() + { + ADD_METHOD( GetMusicSecondsVisible ); + ADD_METHOD( GetSongBeatVisible ); + ADD_METHOD( GetMusicSeconds ); + ADD_METHOD( GetSongBeat ); + ADD_METHOD( GetSongBeatNoOffset ); + ADD_METHOD( GetCurBPS ); + ADD_METHOD( GetFreeze ); + ADD_METHOD( GetDelay ); + ADD_METHOD( GetWarpBeginRow ); + ADD_METHOD( GetWarpDestination ); + } +}; + +LUA_REGISTER_CLASS( SongPosition ); +/** + * @file + * @author Thai Pangsakulyanont (c) 2011 + * @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/SongPosition.h b/src/SongPosition.h new file mode 100644 index 0000000000..830924b84a --- /dev/null +++ b/src/SongPosition.h @@ -0,0 +1,72 @@ +#ifndef SONGPOSITION_H +#define SONGPOSITION_H + +#include "GameConstantsAndTypes.h" +#include "RageTimer.h" +#include "LightsManager.h" +#include "MessageManager.h" +#include "TimingData.h" +//XXX: where does this come from? might need another include +struct lua_State; + +class SongPosition +{ + + public: + + // Arcade - the current stage (one song). + // Oni/Endless - a single song in a course. + // Let a lot of classes access this info here so they don't have to keep their own copies. + float m_fMusicSeconds; // time into the current song, not scaled by music rate + float m_fSongBeat; + float m_fSongBeatNoOffset; + float m_fCurBPS; + float m_fLightSongBeat; // g_fLightsFalloffSeconds ahead + //bool m_bStop; // in the middle of a stop (freeze or delay) + /** @brief A flag to determine if we're in the middle of a freeze/stop. */ + bool m_bFreeze; + /** @brief A flag to determine if we're in the middle of a delay (Pump style stop). */ + bool m_bDelay; + /** @brief The row used to start a warp. */ + int m_iWarpBeginRow; + /** @brief The beat to warp to afterwards. */ + float m_fWarpDestination; + RageTimer m_LastBeatUpdate; // time of last m_fSongBeat, etc. update + float m_fMusicSecondsVisible; + float m_fSongBeatVisible; + + void Reset(); + void UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp = RageZeroTimer ); + + // Lua + void PushSelf( lua_State *L ); +}; + +#endif + +/** + * @file + * @author Thai Pangsakulyanont (c) 2011 + * @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/SoundEffectControl.cpp b/src/SoundEffectControl.cpp index 40d21fa6ba..2055e6bda1 100644 --- a/src/SoundEffectControl.cpp +++ b/src/SoundEffectControl.cpp @@ -44,7 +44,7 @@ void SoundEffectControl::Update( float fDeltaTime ) if( LOCK_TO_HOLD ) { - int iRow = BeatToNoteRow( GAMESTATE->m_fSongBeat ); + int iRow = BeatToNoteRow( GAMESTATE->m_Position.m_fSongBeat ); int iHoldsHeld, iHoldsLetGo; HoldsBeingHeld( iRow, iHoldsHeld, iHoldsLetGo ); diff --git a/src/StepMania-net2008.vcproj b/src/StepMania-net2008.vcproj index fdd1456e75..a038f0dc1d 100644 --- a/src/StepMania-net2008.vcproj +++ b/src/StepMania-net2008.vcproj @@ -1444,6 +1444,14 @@ RelativePath="SongOptions.h" > + + + + diff --git a/src/Steps.cpp b/src/Steps.cpp index 3c0cf7cb57..830b19bac6 100644 --- a/src/Steps.cpp +++ b/src/Steps.cpp @@ -339,6 +339,7 @@ void Steps::AutogenFrom( const Steps *parent_, StepsType ntTo ) { parent = parent_; m_StepsType = ntTo; + m_Timing = parent->m_Timing; } void Steps::CopyFrom( Steps* pSource, StepsType ntTo, float fMusicLengthSeconds ) // pSource does not have to be of the same StepsType @@ -348,6 +349,7 @@ void Steps::CopyFrom( Steps* pSource, StepsType ntTo, float fMusicLengthSeconds pSource->GetNoteData( noteData ); noteData.SetNumTracks( GAMEMAN->GetStepsTypeInfo(ntTo).iNumTracks ); parent = NULL; + m_Timing = pSource->m_Timing; this->SetNoteData( noteData ); this->SetDescription( pSource->GetDescription() ); this->SetDifficulty( pSource->GetDifficulty() ); @@ -400,6 +402,21 @@ void Steps::SetMeter( int meter ) m_iMeter = meter; } +bool Steps::HasSignificantTimingChanges() const +{ + if( m_Timing.HasStops() ) + return true; + + /* TODO: Deal with DisplayBPM here...if possible? + * Song's version may still be useful. */ + + else if( m_Timing.HasBpmChanges() || m_Timing.HasWarps() || m_Timing.HasSpeedChanges() ) + { + return true; + } + return false; +} + void Steps::SetCachedRadarValues( const RadarValues v[NUM_PLAYERS] ) { DeAutogen(); @@ -424,6 +441,8 @@ public: DEFINE_METHOD( IsAnEdit, IsAnEdit() ) DEFINE_METHOD( IsAPlayerEdit, IsAPlayerEdit() ) + static int HasSignificantTimingChanges( T* p, lua_State *L ) { lua_pushboolean(L, p->HasSignificantTimingChanges()); return 1; } + static int GetRadarValues( T* p, lua_State *L ) { PlayerNumber pn = Enum::Check(L, 1); @@ -432,6 +451,12 @@ public: return 1; } + static int GetTimingData( T* p, lua_State *L ) + { + p->m_Timing.PushSelf(L); + return 1; + } + static int GetHash( T* p, lua_State *L ) { lua_pushnumber( L, p->GetHash() ); return 1; } // untested @@ -442,6 +467,7 @@ public: lua_pushstring( L, out ); return 1; } + LunaSteps() { @@ -452,7 +478,9 @@ public: ADD_METHOD( GetFilename ); ADD_METHOD( GetHash ); ADD_METHOD( GetMeter ); + ADD_METHOD( HasSignificantTimingChanges ); ADD_METHOD( GetRadarValues ); + ADD_METHOD( GetTimingData ); //ADD_METHOD( GetSMNoteData ); ADD_METHOD( GetStepsType ); ADD_METHOD( IsAnEdit ); diff --git a/src/Steps.h b/src/Steps.h index fee6204527..078295bc71 100644 --- a/src/Steps.h +++ b/src/Steps.h @@ -8,6 +8,8 @@ #include "Difficulty.h" #include "RageUtil_AutoPtr.h" #include "RageUtil_CachedObject.h" +#include "TimingData.h" + class Profile; class NoteData; struct lua_State; @@ -116,6 +118,14 @@ public: void TidyUpData(); void CalculateRadarValues( float fMusicLengthSeconds ); + /** @brief Timing data */ + TimingData m_Timing; + + /** + * @brief Determine if the Steps have any major timing changes during gameplay. + * @return true if it does, or false otherwise. */ + bool HasSignificantTimingChanges() const; + // Lua void PushSelf( lua_State *L ); diff --git a/src/TimingData.cpp b/src/TimingData.cpp index bb6911fad4..9e7f064cf1 100644 --- a/src/TimingData.cpp +++ b/src/TimingData.cpp @@ -8,10 +8,16 @@ #include -TimingData::TimingData() +TimingData::TimingData() : + m_fBeat0OffsetInSeconds(0), + m_bHasNegativeBpms(false) { - m_fBeat0OffsetInSeconds = 0; - m_bHasNegativeBpms = false; +} + +TimingData::TimingData(float fOffset) : + m_fBeat0OffsetInSeconds(fOffset), + m_bHasNegativeBpms(false) +{ } void TimingData::GetActualBPM( float &fMinBPMOut, float &fMaxBPMOut ) const @@ -62,6 +68,16 @@ void TimingData::AddLabelSegment( const LabelSegment &seg ) m_LabelSegments.insert( upper_bound(m_LabelSegments.begin(), m_LabelSegments.end(), seg), seg ); } +void TimingData::AddSpeedSegment( const SpeedSegment &seg ) +{ + m_SpeedSegments.insert( upper_bound(m_SpeedSegments.begin(), m_SpeedSegments.end(), seg), seg ); +} + +void TimingData::AddFakeSegment( const FakeSegment &seg ) +{ + m_FakeSegments.insert( upper_bound(m_FakeSegments.begin(), m_FakeSegments.end(), seg), seg ); +} + /* Change an existing BPM segment, merge identical segments together or insert a new one. */ void TimingData::SetBPMAtRow( int iNoteRow, float fBPM ) { @@ -160,7 +176,7 @@ void TimingData::SetWarpAtRow( int iRow, float fNew ) for( i=0; i 0 && NoteRowToBeat(iRow) < fNew; + bool valid = iRow > 0 && fNew > 0; if( i == m_WarpSegments.size() ) { if( valid ) @@ -172,7 +188,7 @@ void TimingData::SetWarpAtRow( int iRow, float fNew ) { if( valid ) { - m_WarpSegments[i].m_fEndBeat = fNew; + m_WarpSegments[i].m_fLengthBeats = fNew; } else m_WarpSegments.erase( m_WarpSegments.begin()+i, m_WarpSegments.begin()+i+1 ); @@ -244,6 +260,86 @@ void TimingData::SetLabelAtRow( int iRow, const RString sLabel ) } } +void TimingData::SetSpeedAtRow( int iRow, float fPercent, float fWait, unsigned short usMode ) +{ + unsigned i; + for( i = 0; i < m_SpeedSegments.size(); i++ ) + { + if( m_SpeedSegments[i].m_iStartRow >= iRow) + break; + } + + if ( i == m_SpeedSegments.size() || m_SpeedSegments[i].m_iStartRow != iRow ) + { + // the core mod itself matters the most for comparisons. + if( i == 0 || m_SpeedSegments[i-1].m_fPercent != fPercent ) + AddSpeedSegment( SpeedSegment(iRow, fPercent, fWait, usMode) ); + } + else + { + // The others aren't compared: only the mod itself matters. + if( i > 0 && m_SpeedSegments[i-1].m_fPercent == fPercent ) + m_SpeedSegments.erase( m_SpeedSegments.begin()+i, + m_SpeedSegments.begin()+i+1 ); + else + { + m_SpeedSegments[i].m_fPercent = fPercent; + m_SpeedSegments[i].m_fWait = fWait; + m_SpeedSegments[i].m_usMode = usMode; + } + } +} + +void TimingData::SetFakeAtRow( int iRow, float fNew ) +{ + unsigned i; + for( i=0; i 0 && fNew > 0; + if( i == m_FakeSegments.size() ) + { + if( valid ) + { + AddFakeSegment( FakeSegment(iRow, fNew) ); + } + } + else + { + if( valid ) + { + m_FakeSegments[i].m_fLengthBeats = fNew; + } + else + m_FakeSegments.erase( m_FakeSegments.begin()+i, m_FakeSegments.begin()+i+1 ); + } +} + +void TimingData::SetSpeedPercentAtRow( int iRow, float fPercent ) +{ + SetSpeedAtRow( iRow, + fPercent, + GetSpeedSegmentAtBeat( NoteRowToBeat( iRow ) ).m_fWait, + GetSpeedSegmentAtBeat( NoteRowToBeat( iRow ) ).m_usMode); +} + +void TimingData::SetSpeedWaitAtRow( int iRow, float fWait ) +{ + SetSpeedAtRow( iRow, + GetSpeedSegmentAtBeat( NoteRowToBeat( iRow ) ).m_fPercent, + fWait, + GetSpeedSegmentAtBeat( NoteRowToBeat( iRow ) ).m_usMode); +} + +void TimingData::SetSpeedModeAtRow( int iRow, unsigned short usMode ) +{ + SetSpeedAtRow( iRow, + GetSpeedSegmentAtBeat( NoteRowToBeat( iRow ) ).m_fPercent, + GetSpeedSegmentAtBeat( NoteRowToBeat( iRow ) ).m_fWait, + usMode ); +} + + float TimingData::GetStopAtRow( int iNoteRow, bool bDelay ) const { for( unsigned i=0; i(i); } +int TimingData::GetFakeSegmentIndexAtRow( int iNoteRow ) const +{ + unsigned i; + for( i=0; i iNoteRow ) + break; + } + return static_cast(i); +} + bool TimingData::IsWarpAtRow( int iNoteRow ) const { if( m_WarpSegments.empty() ) @@ -378,7 +513,7 @@ bool TimingData::IsWarpAtRow( int iNoteRow ) const int i = GetWarpSegmentIndexAtRow( iNoteRow ); const WarpSegment& s = m_WarpSegments[i]; - if( s.m_iStartRow <= iNoteRow && iNoteRow < BeatToNoteRow(s.m_fEndBeat) ) + if( s.m_iStartRow <= iNoteRow && iNoteRow < (s.m_iStartRow + BeatToNoteRow(s.m_fLengthBeats) ) ) { if( m_StopSegments.empty() ) { @@ -393,6 +528,20 @@ bool TimingData::IsWarpAtRow( int iNoteRow ) const return false; } +bool TimingData::IsFakeAtRow( int iNoteRow ) const +{ + if( m_FakeSegments.empty() ) + return false; + + int i = GetFakeSegmentIndexAtRow( iNoteRow ); + const FakeSegment& s = m_FakeSegments[i]; + if( s.m_iStartRow <= iNoteRow && iNoteRow < ( s.m_iStartRow + BeatToNoteRow(s.m_fLengthBeats) ) ) + { + return true; + } + return false; +} + int TimingData::GetTimeSignatureSegmentIndexAtRow( int iRow ) const { unsigned i; @@ -426,6 +575,15 @@ int TimingData::GetLabelSegmentIndexAtRow( int iRow ) const return static_cast(i); } +int TimingData::GetSpeedSegmentIndexAtRow( int iRow ) const +{ + unsigned i; + for (i=0; i < m_SpeedSegments.size() - 1; i++ ) + if( m_SpeedSegments[i+1].m_iStartRow > iRow ) + break; + return static_cast(i); +} + BPMSegment& TimingData::GetBPMSegmentAtRow( int iNoteRow ) { static BPMSegment empty; @@ -445,6 +603,15 @@ TimeSignatureSegment& TimingData::GetTimeSignatureSegmentAtRow( int iRow ) return m_vTimeSignatureSegments[i]; } +SpeedSegment& TimingData::GetSpeedSegmentAtRow( int iRow ) +{ + unsigned i; + for( i=0; i iRow ) + break; + return m_SpeedSegments[i]; +} + int TimingData::GetTimeSignatureNumeratorAtRow( int iRow ) { return GetTimeSignatureSegmentAtRow( iRow ).m_iNumerator; @@ -493,6 +660,16 @@ WarpSegment& TimingData::GetWarpSegmentAtRow( int iRow ) return m_WarpSegments[i]; } +FakeSegment& TimingData::GetFakeSegmentAtRow( int iRow ) +{ + static FakeSegment empty; + if( m_FakeSegments.empty() ) + return empty; + + int i = GetFakeSegmentIndexAtRow( iRow ); + return m_FakeSegments[i]; +} + int TimingData::GetTickcountSegmentIndexAtRow( int iRow ) const { unsigned i; @@ -652,15 +829,18 @@ void TimingData::GetBeatAndBPSFromElapsedTimeNoOffset( float fElapsedTime, float } break; case FOUND_WARP: - bIsWarping = true; - if( itWS->m_fEndBeat > fWarpDestination ) { - fWarpDestination = itWS->m_fEndBeat; + bIsWarping = true; + float fWarpSum = itWS->m_fLengthBeats + NoteRowToBeat( itWS->m_iStartRow ); + if( fWarpSum > fWarpDestination ) + { + fWarpDestination = fWarpSum; + } + iWarpBeginOut = iEventRow; + fWarpDestinationOut = fWarpDestination; + itWS ++; + break; } - iWarpBeginOut = iEventRow; - fWarpDestinationOut = fWarpDestination; - itWS ++; - break; } iLastRow = iEventRow; } @@ -744,13 +924,16 @@ float TimingData::GetElapsedTimeFromBeatNoOffset( float fBeat ) const case FOUND_MARKER: return fLastTime; case FOUND_WARP: - bIsWarping = true; - if( itWS->m_fEndBeat > fWarpDestination ) { - fWarpDestination = itWS->m_fEndBeat; + bIsWarping = true; + float fWarpSum = itWS->m_fLengthBeats + NoteRowToBeat( itWS->m_iStartRow ); + if( fWarpSum > fWarpDestination ) + { + fWarpDestination = fWarpSum; + } + itWS ++; + break; } - itWS ++; - break; } iLastRow = iEventRow; } @@ -790,13 +973,13 @@ void TimingData::ScaleRegion( float fScale, int iStartIndex, int iEndIndex, bool for( unsigned i = 0; i < m_WarpSegments.size(); i++ ) { const int iSegStartRow = m_WarpSegments[i].m_iStartRow; - const int iSegEndRow = BeatToNoteRow( m_WarpSegments[i].m_fEndBeat ); + const int iSegEndRow = iSegStartRow + BeatToNoteRow( m_WarpSegments[i].m_fLengthBeats ); if( iSegEndRow >= iStartIndex ) { if( iSegEndRow > iEndIndex ) - m_WarpSegments[i].m_fEndBeat += NoteRowToBeat(lrintf((iEndIndex - iStartIndex) * (fScale - 1))); + m_WarpSegments[i].m_fLengthBeats += NoteRowToBeat(lrintf((iEndIndex - iStartIndex) * (fScale - 1))); else - m_WarpSegments[i].m_fEndBeat = NoteRowToBeat(lrintf((iSegEndRow - iStartIndex) * fScale) + iStartIndex); + m_WarpSegments[i].m_fLengthBeats = NoteRowToBeat(lrintf((iSegEndRow - iStartIndex) * fScale)); } if( iSegStartRow < iStartIndex ) continue; @@ -839,6 +1022,36 @@ void TimingData::ScaleRegion( float fScale, int iStartIndex, int iEndIndex, bool m_LabelSegments[i].m_iStartRow = lrintf( (iSegStart - iStartIndex) * fScale ) + iStartIndex; } + for ( unsigned i = 0; i < m_SpeedSegments.size(); i++ ) + { + const int iSegStart = m_SpeedSegments[i].m_iStartRow; + if( iSegStart < iStartIndex ) + continue; + else if( iSegStart > iEndIndex ) + m_SpeedSegments[i].m_iStartRow += lrintf( (iEndIndex - iStartIndex) * (fScale - 1) ); + else + m_SpeedSegments[i].m_iStartRow = lrintf( (iSegStart - iStartIndex) * fScale ) + iStartIndex; + } + + for( unsigned i = 0; i < m_FakeSegments.size(); i++ ) + { + const int iSegStartRow = m_FakeSegments[i].m_iStartRow; + const int iSegEndRow = iSegStartRow + BeatToNoteRow( m_FakeSegments[i].m_fLengthBeats ); + if( iSegEndRow >= iStartIndex ) + { + if( iSegEndRow > iEndIndex ) + m_FakeSegments[i].m_fLengthBeats += NoteRowToBeat(lrintf((iEndIndex - iStartIndex) * (fScale - 1))); + else + m_FakeSegments[i].m_fLengthBeats = NoteRowToBeat(lrintf((iSegEndRow - iStartIndex) * fScale)); + } + if( iSegStartRow < iStartIndex ) + continue; + else if( iSegStartRow > iEndIndex ) + m_FakeSegments[i].m_iStartRow += lrintf((iEndIndex - iStartIndex) * (fScale - 1)); + else + m_FakeSegments[i].m_iStartRow = lrintf((iSegStartRow - iStartIndex) * fScale) + iStartIndex; + } + // adjust BPM changes to preserve timing if( bAdjustBPM ) { @@ -886,8 +1099,6 @@ void TimingData::InsertRows( int iStartRow, int iRowsToAdd ) for( unsigned i = 0; i < m_WarpSegments.size(); i++ ) { WarpSegment &warp = m_WarpSegments[i]; - if( BeatToNoteRow(warp.m_fEndBeat) >= iStartRow ) - warp.m_fEndBeat += NoteRowToBeat(iRowsToAdd); if( warp.m_iStartRow < iStartRow ) continue; warp.m_iStartRow += iRowsToAdd; @@ -923,6 +1134,22 @@ void TimingData::InsertRows( int iStartRow, int iRowsToAdd ) continue; labl.m_iStartRow += iRowsToAdd; } + + for( unsigned i = 0; i < m_SpeedSegments.size(); i++ ) + { + SpeedSegment &sped = m_SpeedSegments[i]; + if( sped.m_iStartRow < iStartRow ) + continue; + sped.m_iStartRow += iRowsToAdd; + } + + for( unsigned i = 0; i < m_FakeSegments.size(); i++ ) + { + FakeSegment &fake = m_FakeSegments[i]; + if( fake.m_iStartRow < iStartRow ) + continue; + fake.m_iStartRow += iRowsToAdd; + } if( iStartRow == 0 ) { @@ -985,9 +1212,6 @@ void TimingData::DeleteRows( int iStartRow, int iRowsToDelete ) { WarpSegment &warp = m_WarpSegments[i]; - if( BeatToNoteRow(warp.m_fEndBeat) >= iStartRow ) - warp.m_fEndBeat = max( NoteRowToBeat(iStartRow), warp.m_fEndBeat - NoteRowToBeat(iRowsToDelete) ); - if( warp.m_iStartRow < iStartRow ) continue; @@ -1080,10 +1304,99 @@ void TimingData::DeleteRows( int iStartRow, int iRowsToDelete ) } labl.m_iStartRow -= iRowsToDelete; } + + for( unsigned i = 0; i < m_SpeedSegments.size(); i++ ) + { + SpeedSegment &sped = m_SpeedSegments[i]; + + if( sped.m_iStartRow < iStartRow ) + continue; + + if( sped.m_iStartRow < iStartRow+iRowsToDelete ) + { + m_SpeedSegments.erase( m_SpeedSegments.begin()+i, m_SpeedSegments.begin()+i+1 ); + --i; + continue; + } + sped.m_iStartRow -= iRowsToDelete; + } + + for( unsigned i = 0; i < m_FakeSegments.size(); i++ ) + { + FakeSegment &fake = m_FakeSegments[i]; + + if( fake.m_iStartRow < iStartRow ) + continue; + + if( fake.m_iStartRow < iStartRow+iRowsToDelete ) + { + m_FakeSegments.erase( m_FakeSegments.begin()+i, m_FakeSegments.begin()+i+1 ); + --i; + continue; + } + + fake.m_iStartRow -= iRowsToDelete; + } this->SetBPMAtRow( iStartRow, fNewBPM ); } +void TimingData::TidyUpData() +{ + // If there are no BPM segments, provide a default. + if( m_BPMSegments.empty() ) + { + LOG->UserLog( "Song file", m_sFile, "has no BPM segments, default provided." ); + + AddBPMSegment( BPMSegment(0, 60) ); + } + + // Make sure the first BPM segment starts at beat 0. + if( m_BPMSegments[0].m_iStartRow != 0 ) + m_BPMSegments[0].m_iStartRow = 0; + + // If no time signature specified, assume 4/4 time for the whole song. + if( m_vTimeSignatureSegments.empty() ) + { + TimeSignatureSegment seg(0, 4, 4); + m_vTimeSignatureSegments.push_back( seg ); + } + + // Likewise, if no tickcount signature is specified, assume 2 ticks + //per beat for the entire song. The default of 2 is chosen more + //for compatibility with the Pump Pro series than anything else. + if( m_TickcountSegments.empty() ) + { + TickcountSegment seg(0, 2); + m_TickcountSegments.push_back( seg ); + } + + // Have a default combo segment of one just in case. + if( m_ComboSegments.empty() ) + { + ComboSegment seg(0, 1); + m_ComboSegments.push_back( seg ); + } + + // Have a default label segment just in case. + if( m_LabelSegments.empty() ) + { + LabelSegment seg(0, "Song Start"); + m_LabelSegments.push_back( seg ); + } + + // Always be sure there is a starting speed. + if( m_SpeedSegments.empty() ) + { + SpeedSegment seg(0, 1, 0); + m_SpeedSegments.push_back( seg ); + } +} + + + + + bool TimingData::HasBpmChanges() const { return m_BPMSegments.size()>1; @@ -1099,6 +1412,16 @@ bool TimingData::HasWarps() const return m_WarpSegments.size()>0; } +bool TimingData::HasFakes() const +{ + return m_FakeSegments.size()>0; +} + +bool TimingData::HasSpeedChanges() const +{ + return m_SpeedSegments.size()>1; +} + void TimingData::NoteRowToMeasureAndBeat( int iNoteRow, int &iMeasureIndexOut, int &iBeatIndexOut, int &iRowsRemainder ) const { iMeasureIndexOut = 0; @@ -1145,6 +1468,8 @@ public: static int HasStops( T* p, lua_State *L ) { lua_pushboolean(L, p->HasStops()); return 1; } static int HasBPMChanges( T* p, lua_State *L ) { lua_pushboolean(L, p->HasBpmChanges()); return 1; } static int HasWarps( T* p, lua_State *L ) { lua_pushboolean(L, p->HasWarps()); return 1; } + static int HasFakes( T* p, lua_State *L ) { lua_pushboolean(L, p->HasFakes()); return 1; } + static int HasSpeedChanges( T* p, lua_State *L ) { lua_pushboolean(L, p->HasSpeedChanges()); return 1; } static int GetStops( T* p, lua_State *L ) { vector vStops; @@ -1232,6 +1557,8 @@ public: ADD_METHOD( HasStops ); ADD_METHOD( HasBPMChanges ); ADD_METHOD( HasWarps ); + ADD_METHOD( HasFakes ); + ADD_METHOD( HasSpeedChanges ); ADD_METHOD( GetStops ); ADD_METHOD( GetDelays ); ADD_METHOD( GetBPMs ); diff --git a/src/TimingData.h b/src/TimingData.h index 5464a8748e..9b8889b207 100644 --- a/src/TimingData.h +++ b/src/TimingData.h @@ -313,8 +313,8 @@ struct TimeSignatureSegment * * A warp segment is used to replicate the effects of Negative BPMs without * abusing negative BPMs. Negative BPMs should be converted to warp segments. - * WarpAt=WarpTo is the format, where both are in beats. (Technically they're - * both rows though.) */ + * WarpAt=WarpToRelative is the format, where both are in beats. + * (Technically they're both rows though.) */ struct WarpSegment { /** @@ -322,45 +322,45 @@ struct WarpSegment * * It is best to override the values as soon as possible. */ - WarpSegment() : m_iStartRow(-1), m_fEndBeat(-1) { } + WarpSegment() : m_iStartRow(-1), m_fLengthBeats(-1) { } /** * @brief Create a Warp Segment with the specified starting row and row to warp to. * @param s the starting row of this segment. - * @param r the row to warp to. + * @param r the number of rows to jump ahead. */ - WarpSegment( int s, int r ): m_iStartRow(max(0, (s < r ? s : r))), - m_fEndBeat(max(0, NoteRowToBeat((r > s ? r : s)))) {} + WarpSegment( int s, int r ): m_iStartRow(s), + m_fLengthBeats(NoteRowToBeat(r)) {} /** * @brief Creates a Warp Segment with the specified starting row and beat to warp to. * @param s the starting row of this segment. - * @param b the beat to warp to. + * @param b the number of beats to jump ahead. */ WarpSegment( int s, float b ): m_iStartRow(max(0, s)), - m_fEndBeat(max(0, b)) {} + m_fLengthBeats(max(0, b)) {} /** * @brief Create a Warp Segment with the specified starting beat and row to warp to. * @param s the starting beat in this segment. - * @param r the row to warp to. + * @param r the number of rows to jump ahead. */ WarpSegment( float s, int r ): m_iStartRow(max(0, BeatToNoteRow(s))), - m_fEndBeat(max(0, NoteRowToBeat(r))) {} + m_fLengthBeats(max(0, NoteRowToBeat(r))) {} /** * @brief Creates a Warp Segment with the specified starting beat and beat to warp to. * @param s the starting beat of this segment. - * @param b the beat to warp to. + * @param b the number of beats to jump ahead. */ WarpSegment( float s, float b ): - m_iStartRow(max(0, BeatToNoteRow((s < b ? s : b)))), - m_fEndBeat(max(0, (b > s ? b : s))) {} + m_iStartRow(BeatToNoteRow(s)), + m_fLengthBeats(b) {} /** * @brief The row in which the WarpSegment activates. */ int m_iStartRow; /** - * @brief The beat to warp to. + * @brief The number of beats to warp ahead by. */ - float m_fEndBeat; + float m_fLengthBeats; /** * @brief Compares two WarpSegments to see if they are equal to each other. * @param other the other WarpSegment to compare to. @@ -369,7 +369,7 @@ struct WarpSegment bool operator==( const WarpSegment &other ) const { COMPARE( m_iStartRow ); - COMPARE( m_fEndBeat ); + COMPARE( m_fLengthBeats ); return true; } /** @@ -386,7 +386,7 @@ struct WarpSegment bool operator<( const WarpSegment &other ) const { return m_iStartRow < other.m_iStartRow || - ( m_iStartRow == other.m_iStartRow && m_fEndBeat < other.m_fEndBeat ); + ( m_iStartRow == other.m_iStartRow && m_fLengthBeats < other.m_fLengthBeats ); } /** * @brief Compares two WarpSegments to see if one is less than or equal to the other. @@ -508,7 +508,7 @@ struct ComboSegment * @param t the amount the combo increases on a succesful hit. */ ComboSegment( int s, int t ): m_iStartRow(max(0, s)), - m_iCombo(max(1,t)) {} + m_iCombo(max(0,t)) {} /** * @brief The row in which the ComboSegment activates. */ @@ -647,6 +647,243 @@ struct LabelSegment bool operator>=( const LabelSegment &other ) const { return !operator<(other); } }; +/** + * @brief Identifies when the arrow scroll changes. + * + * SpeedSegments take a Player's scrolling BPM (Step's BPM * speed mod), + * and then multiplies it with the percentage value. No matter the player's + * speed mod, the ratio will be the same. Unlike forced attacks, these + * cannot be turned off at a set time: reset it by setting the precentage + * back to 1. + * + * These were inspired by the Pump It Up series. */ +struct SpeedSegment +{ + /** @brief Sets up the SpeedSegment with default values. */ + SpeedSegment(): m_iStartRow(0), + m_fPercent(1), m_fWait(0), m_usMode(0) {} + + /** + * @brief Sets up the SpeedSegment with specified values. + * @param i The row this activates. + * @param p The percentage to use. */ + SpeedSegment(int i, float p): m_iStartRow(0), + m_fPercent(p), m_fWait(0), m_usMode(0) {} + + /** + * @brief Sets up the SpeedSegment with specified values. + * @param r The beat this activates. + * @param p The percentage to use. */ + SpeedSegment(float r, float p): m_iStartRow(BeatToNoteRow(r)), + m_fPercent(p), m_fWait(0), m_usMode(0) {} + + /** + * @brief Sets up the SpeedSegment with specified values. + * @param i The row this activates. + * @param p The percentage to use. + * @param w The number of beats to wait. */ + SpeedSegment(int i, float p, float w): m_iStartRow(i), + m_fPercent(p), m_fWait(w), m_usMode(0) {} + + /** + * @brief Sets up the SpeedSegment with specified values. + * @param r The beat this activates. + * @param p The percentage to use. + * @param w The number of beats to wait. */ + SpeedSegment(float r, float p, float w): m_iStartRow(BeatToNoteRow(r)), + m_fPercent(p), m_fWait(w), m_usMode(0) {} + + /** + * @brief Sets up the SpeedSegment with specified values. + * @param i The row this activates. + * @param p The percentage to use. + * @param w The number of beats/seconds to wait. + * @param k The mode used for the wait variable. */ + SpeedSegment(int i, float p, float w, unsigned short k): m_iStartRow(i), + m_fPercent(p), m_fWait(w), m_usMode(k) {} + + /** + * @brief Sets up the SpeedSegment with specified values. + * @param r The beat this activates. + * @param p The percentage to use. + * @param w The number of beats/seconds to wait. + * @param k The mode used for the wait variable.*/ + SpeedSegment(float r, float p, float w, unsigned short k): m_iStartRow(BeatToNoteRow(r)), + m_fPercent(p), m_fWait(w), m_usMode(k) {} + + /** @brief The row in which the ComboSegment activates. */ + int m_iStartRow; + /** @brief The percentage to use when multiplying the Player's BPM. */ + float m_fPercent; + /** + * @brief The number of beats or seconds to wait for the change to take place. + * + * A value of 0 means this is immediate. */ + float m_fWait; + /** + * @brief The mode that this segment uses for the math. + * + * 0: beats + * 1: seconds + * other + */ + unsigned short m_usMode; + + /** + * @brief Compares two SpeedSegments to see if they are equal to each other. + * @param other the other SpeedSegment to compare to. + * @return the equality of the two segments. + */ + bool operator==( const SpeedSegment &other ) const + { + COMPARE( m_iStartRow ); + COMPARE( m_fPercent ); + COMPARE( m_usMode ); + COMPARE( m_fWait ); + return true; + } + /** + * @brief Compares two SpeedSegments to see if they are not equal to each other. + * @param other the other SpeedSegment to compare to. + * @return the inequality of the two segments. + */ + bool operator!=( const SpeedSegment &other ) const { return !operator==(other); } + /** + * @brief Compares two SpeedSegments to see if one is less than the other. + * @param other the other SpeedSegment to compare to. + * @return the truth/falsehood of if the first is less than the second. + */ + bool operator<( const SpeedSegment &other ) const { return m_iStartRow < other.m_iStartRow; } + /** + * @brief Compares two SpeedSegments to see if one is less than or equal to the other. + * @param other the other SpeedSegment to compare to. + * @return the truth/falsehood of if the first is less or equal to than the second. + */ + bool operator<=( const SpeedSegment &other ) const + { + return ( operator<(other) || operator==(other) ); + } + /** + * @brief Compares two SpeedSegments to see if one is greater than the other. + * @param other the other SpeedSegment to compare to. + * @return the truth/falsehood of if the first is greater than the second. + */ + bool operator>( const SpeedSegment &other ) const { return !operator<=(other); } + /** + * @brief Compares two SpeedSegments to see if one is greater than or equal to the other. + * @param other the other SpeedSegment to compare to. + * @return the truth/falsehood of if the first is greater than or equal to the second. + */ + bool operator>=( const SpeedSegment &other ) const { return !operator<(other); } +}; + +/** + * @brief Identifies when a whole region of arrows is to be ignored. + * + * FakeSegments are similar to the Fake Tap Notes in that the contents + * inside are neither for nor against the player. They can be useful for + * mission modes, in conjunction with WarpSegments, or perhaps other + * uses not thought up at the time of this comment. Unlike the Warp + * Segments, these are not magically jumped over: instead, these are + * drawn normally. + * + * These were inspired by the Pump It Up series. */ +struct FakeSegment +{ + /** + * @brief Create a simple Fake Segment with default values. + * + * It is best to override the values as soon as possible. + */ + FakeSegment() : m_iStartRow(-1), m_fLengthBeats(-1) { } + /** + * @brief Create a Fake Segment with the specified values. + * @param s the starting row of this segment. + * @param r the number of rows this segment lasts. + */ + FakeSegment( int s, int r ): m_iStartRow(s), + m_fLengthBeats(NoteRowToBeat(r)) {} + /** + * @brief Creates a Fake Segment with the specified values. + * @param s the starting row of this segment. + * @param b the number of beats this segment lasts. + */ + FakeSegment( int s, float b ): m_iStartRow(max(0, s)), + m_fLengthBeats(max(0, b)) {} + /** + * @brief Create a Fake Segment with the specified values. + * @param s the starting beat in this segment. + * @param r the number of rows this segment lasts. + */ + FakeSegment( float s, int r ): + m_iStartRow(max(0, BeatToNoteRow(s))), + m_fLengthBeats(max(0, NoteRowToBeat(r))) {} + /** + * @brief Creates a Fake Segment with the specified values. + * @param s the starting beat of this segment. + * @param b the number of beats this segment lasts. + */ + FakeSegment( float s, float b ): + m_iStartRow(BeatToNoteRow(s)), + m_fLengthBeats(b) {} + /** + * @brief The row in which the FakeSegment activates. + */ + int m_iStartRow; + /** + * @brief The number of beats the FakeSegment is alive for. + */ + float m_fLengthBeats; + /** + * @brief Compares two FakeSegments to see if they are equal to each other. + * @param other the other FakeSegment to compare to. + * @return the equality of the two segments. + */ + bool operator==( const FakeSegment &other ) const + { + COMPARE( m_iStartRow ); + COMPARE( m_fLengthBeats ); + return true; + } + /** + * @brief Compares two FakeSegments to see if they are not equal to each other. + * @param other the other FakeSegment to compare to. + * @return the inequality of the two segments. + */ + bool operator!=( const FakeSegment &other ) const { return !operator==(other); } + /** + * @brief Compares two FakeSegments to see if one is less than the other. + * @param other the other FakeSegment to compare to. + * @return the truth/falsehood of if the first is less than the second. + */ + bool operator<( const FakeSegment &other ) const + { + return m_iStartRow < other.m_iStartRow || + ( m_iStartRow == other.m_iStartRow && m_fLengthBeats < other.m_fLengthBeats ); + } + /** + * @brief Compares two FakeSegments to see if one is less than or equal to the other. + * @param other the other FakeSegment to compare to. + * @return the truth/falsehood of if the first is less or equal to than the second. + */ + bool operator<=( const FakeSegment &other ) const + { + return ( operator<(other) || operator==(other) ); + } + /** + * @brief Compares two FakeSegments to see if one is greater than the other. + * @param other the other FakeSegment to compare to. + * @return the truth/falsehood of if the first is greater than the second. + */ + bool operator>( const FakeSegment &other ) const { return !operator<=(other); } + /** + * @brief Compares two FakeSegments to see if one is greater than or equal to the other. + * @param other the other FakeSegment to compare to. + * @return the truth/falsehood of if the first is greater than or equal to the second. + */ + bool operator>=( const FakeSegment &other ) const { return !operator<(other); } +}; + /** * @brief Holds data for translating beats<->seconds. @@ -658,6 +895,10 @@ public: * @brief Sets up initial timing data. */ TimingData(); + /** + * @brief Sets up initial timing data with a defined offset. + * @param fOffset the offset from the 0th beat. */ + TimingData(float fOffset); /** * @brief Gets the actual BPM of the song. * @param fMinBPMOut the minimium specified BPM. @@ -1234,6 +1475,193 @@ public: */ float GetNextLabelSegmentBeatAtBeat( float fBeat ) const { return GetNextLabelSegmentBeatAtRow( BeatToNoteRow(fBeat) ); } + + /** + * @brief Retrieve the Speed's percent at the given row. + * @param iNoteRow the row in question. + * @return the percent. + */ + float GetSpeedPercentAtRow( int iNoteRow ); + /** + * @brief Retrieve the Speed's percent at the given beat. + * @param fBeat the beat in question. + * @return the percent. + */ + float GetSpeedPercentAtBeat( float fBeat ) { return GetSpeedPercentAtRow( BeatToNoteRow(fBeat) ); } + /** + * @brief Retrieve the Speed's wait at the given row. + * @param iNoteRow the row in question. + * @return the wait. + */ + float GetSpeedWaitAtRow( int iNoteRow ); + /** + * @brief Retrieve the Speed's wait at the given beat. + * @param fBeat the beat in question. + * @return the wait. + */ + float GetSpeedWaitAtBeat( float fBeat ) { return GetSpeedWaitAtRow( BeatToNoteRow(fBeat) ); } + /** + * @brief Retrieve the Speed's mode at the given row. + * @param iNoteRow the row in question. + * @return the mode. + */ + unsigned short GetSpeedModeAtRow( int iNoteRow ); + /** + * @brief Retrieve the Speed's mode at the given beat. + * @param fBeat the beat in question. + * @return the mode. + */ + unsigned short GetSpeedModeAtBeat( float fBeat ) { return GetSpeedModeAtRow( BeatToNoteRow(fBeat) ); } + /** + * @brief Set the row to have the new Speed. + * @param iNoteRow the row to have the new Speed. + * @param fPercent the percent. + * @param fWait the wait. + * @param usMode the mode. + */ + void SetSpeedAtRow( int iNoteRow, float fPercent, float fWait, unsigned short usMode ); + /** + * @brief Set the beat to have the new Speed. + * @param fBeat the beat to have the new Speed. + * @param fPercent the percent. + * @param fWait the wait. + * @param usMode the mode. + */ + void SetSpeedAtBeat( float fBeat, float fPercent, float fWait, unsigned short usMode ) { SetSpeedAtRow( BeatToNoteRow(fBeat), fPercent, fWait, usMode ); } + /** + * @brief Set the row to have the new Speed percent. + * @param iNoteRow the row to have the new Speed percent. + * @param fPercent the percent. + */ + void SetSpeedPercentAtRow( int iNoteRow, float fPercent ); + /** + * @brief Set the beat to have the new Speed percent. + * @param fBeat the beat to have the new Speed percent. + * @param fPercent the percent. + */ + void SetSpeedPercentAtBeat( float fBeat, float fPercent ) { SetSpeedPercentAtRow( BeatToNoteRow(fBeat), fPercent); } + /** + * @brief Set the row to have the new Speed wait. + * @param iNoteRow the row to have the new Speed wait. + * @param fWait the wait. + */ + void SetSpeedWaitAtRow( int iNoteRow, float fWait ); + /** + * @brief Set the beat to have the new Speed wait. + * @param fBeat the beat to have the new Speed wait. + * @param fWait the wait. + */ + void SetSpeedWaitAtBeat( float fBeat, float fWait ) { SetSpeedWaitAtRow( BeatToNoteRow(fBeat), fWait); } + /** + * @brief Set the row to have the new Speed mode. + * @param iNoteRow the row to have the new Speed mode. + * @param usMode the mode. + */ + void SetSpeedModeAtRow( int iNoteRow, unsigned short usMode ); + /** + * @brief Set the beat to have the new Speed mode. + * @param fBeat the beat to have the new Speed mode. + * @param usMode the mode. + */ + void SetSpeedModeAtBeat( float fBeat, unsigned short usMode ) { SetSpeedModeAtRow( BeatToNoteRow(fBeat), usMode); } + /** + * @brief Retrieve the SpeedSegment at the specified row. + * @param iNoteRow the row that has a SpeedSegment. + * @return the SpeedSegment in question. + */ + SpeedSegment& GetSpeedSegmentAtRow( int iNoteRow ); + /** + * @brief Retrieve the SpeedSegment at the specified beat. + * @param fBeat the beat that has a SpeedSegment. + * @return the SpeedSegment in question. + */ + SpeedSegment& GetSpeedSegmentAtBeat( float fBeat ) { return GetSpeedSegmentAtRow( BeatToNoteRow(fBeat) ); } + /** + * @brief Retrieve the index of the SpeedSegments at the specified row. + * @param iNoteRow the row that has a SpeedSegment. + * @return the SpeedSegment's index in question. + */ + int GetSpeedSegmentIndexAtRow( int iNoteRow ) const; + /** + * @brief Retrieve the index of the SpeedSegments at the specified beat. + * @param fBeat the beat that has a SpeedSegment. + * @return the SpeedSegment's index in question. + */ + int GetSpeedSegmentIndexAtBeat( float fBeat ) const { return GetSpeedSegmentIndexAtRow( BeatToNoteRow(fBeat) ); } + /** + * @brief Add the SpeedSegment to the TimingData. + * @param seg the new SpeedSegment. + */ + void AddSpeedSegment( const SpeedSegment &seg ); + + /** + * @brief Determine when the fakes end. + * @param iRow The row you start on. + * @return the time when the fakes end. + */ + float GetFakeAtRow( int iRow ) const; + /** + * @brief Determine when the fakes end. + * @param fBeat The beat you start on. + * @return the time when the fakes end. + */ + float GetFakeAtBeat( float fBeat ) const { return GetFakeAtRow( BeatToNoteRow( fBeat ) ); } + /** + * @brief Set the beat to indicate when the FakeSegment ends. + * @param iRow The row to start on. + * @param fNew The destination beat. + */ + void SetFakeAtRow( int iRow, float fNew ); + /** + * @brief Set the beat to indicate when the FakeSegment ends. + * @param fBeat The beat to start on. + * @param fNew The destination beat. + */ + void SetFakeAtBeat( float fBeat, float fNew ) { SetFakeAtRow( BeatToNoteRow( fBeat ), fNew ); } + /** + * @brief Retrieve the FakeSegment at the specified row. + * @param iRow the row to focus on. + * @return the FakeSegment in question. + */ + FakeSegment& GetFakeSegmentAtRow( int iRow ); + /** + * @brief Retrieve the FakeSegment at the specified beat. + * @param fBeat the beat to focus on. + * @return the FakeSegment in question. + */ + FakeSegment& GetFakeSegmentAtBeat( float fBeat ) { return GetFakeSegmentAtRow( BeatToNoteRow( fBeat ) ); } + /** + * @brief Retrieve the index of the FakeSegment at the specified row. + * @param iRow the row to focus on. + * @return the index in question. + */ + int GetFakeSegmentIndexAtRow( int iRow ) const; + /** + * @brief Retrieve the index of the FakeSegment at the specified beat. + * @param fBeat the beat to focus on. + * @return the index in question. + */ + int GetFakeSegmentIndexAtBeat( float fBeat ) const { return GetFakeSegmentIndexAtRow( BeatToNoteRow( fBeat ) ); } + /** + * @brief Checks if the row is inside a fake. + * @param iRow the row to focus on. + * @return true if the row is inside a fake, false otherwise. + */ + bool IsFakeAtRow( int iRow ) const; + /** + * @brief Checks if the beat is inside a fake. + * @param fBeat the beat to focus on. + * @return true if the row is inside a fake, false otherwise. + */ + bool IsFakeAtBeat( float fBeat ) const { return IsFakeAtRow( BeatToNoteRow( fBeat ) ); } + /** + * @brief Add the FakeSegment to the TimingData. + * @param seg the new FakeSegment. + */ + void AddFakeSegment( const FakeSegment &seg ); + + + void MultiplyBPMInBeatRange( int iStartIndex, int iEndIndex, float fFactor ); void NoteRowToMeasureAndBeat( int iNoteRow, int &iMeasureIndexOut, int &iBeatIndexOut, int &iRowsRemainder ) const; @@ -1276,6 +1704,14 @@ public: * @return true if there is at least one warp, false otherwise. */ bool HasWarps() const; + /** + * @brief View the TimingData to see if there is at least one fake segment involved. + * @return true if there is at least one fake segment, false otherwise. */ + bool HasFakes() const; + /** + * @brief View the TimingData to see if a song changes its speed scrolling at any point. + * @return true if there is at least one change, false otherwise. */ + bool HasSpeedChanges() const; /** * @brief Compare two sets of timing data to see if they are equal. * @param other the other TimingData. @@ -1301,8 +1737,15 @@ public: COMPARE( m_ComboSegments.size() ); for( unsigned i=0; i m_LabelSegments; + /** @brief The collection of SpeedSegments. */ + vector m_SpeedSegments; + /** @brief The collection of FakeSegments. */ + vector m_FakeSegments; /** * @brief The initial offset of a song. */