Last active
December 12, 2015 00:58
-
-
Save zardoru/4687350 to your computer and use it in GitHub Desktop.
notes exporter to osu for stepmania. yes this is my work with help of several people for getting the osu file format right.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "global.h" | |
#include "NoteTypes.h" | |
#include "NoteData.h" | |
#include "RageUtil.h" | |
#include "RageLog.h" | |
#include "RageFileManager.h" | |
#include "RageFile.h" | |
#include "NoteDataUtil.h" | |
#include "RageFile.h" | |
#include "Song.h" | |
#include "Steps.h" | |
#include "NotesWriterOsu.h" | |
static void WriteGlobalTags( RageFile &f, const Song &out, Steps* ToSave ) | |
{ | |
f.PutLine("osu file format v11"); // double space | |
f.PutLine(""); | |
f.PutLine("[General]"); | |
f.PutLine( ssprintf("AudioFilename: %s", out.m_sMusicFile.c_str()) ); | |
f.PutLine("AudioLeadIn: 1500"); | |
f.PutLine( ssprintf("PreviewTime: %.0f", out.m_fMusicSampleStartSeconds * 1000) ); | |
f.PutLine("Countdown: 0"); | |
f.PutLine("SampleSet: None"); | |
f.PutLine("StackLeniency: 0.7"); | |
f.PutLine("Mode: 3"); | |
f.PutLine("LetterboxInBreaks: 0"); | |
f.PutLine(""); | |
f.PutLine("[Editor]"); | |
f.PutLine("DistanceSnapping: 0.9"); | |
f.PutLine("BeatDivisor: 4"); | |
f.PutLine("GridSize: 16"); | |
f.PutLine("CurrentTime: 0"); | |
f.PutLine(""); | |
f.PutLine("[Metadata]"); | |
f.PutLine( ssprintf("Title: %s", out.m_sMainTitleTranslit.length() ? out.m_sMainTitleTranslit.c_str() : out.m_sMainTitle.c_str() ) ); | |
f.PutLine( ssprintf("TitleUnicode: %s", out.m_sMainTitle.c_str()) ); | |
f.PutLine( ssprintf("Artist: %s", out.m_sArtistTranslit.length() ? out.m_sArtistTranslit.c_str() : out.m_sArtist.c_str() ) ); | |
f.PutLine( ssprintf("ArtistUnicode: %s", out.m_sArtist.c_str()) ); | |
f.PutLine( ssprintf("Creator: %s", out.m_sCredit.length() ? out.m_sCredit.c_str() : "Stepmania 5" ) ); | |
f.PutLine( ssprintf("Version: %s", ToSave->GetChartName().c_str()) ); | |
f.PutLine( "Source: " ); | |
f.PutLine( "Tags: " ); | |
// fixme: what if we want to save the original .osu's beatmap IDs? | |
// does it get updated automatically on osu's end? | |
f.PutLine( "BeatmapID:0" ); | |
f.PutLine( "BeatmapSetID:-1"); | |
f.PutLine(""); | |
f.PutLine( "[Difficulty]" ); | |
f.PutLine( "HPDrainRate: 6" ); | |
f.PutLine( ssprintf("CircleSize: %i", ToSave->GetNoteData().GetNumTracks()) ); | |
f.PutLine( "OverallDifficulty: 5" ); | |
f.PutLine( "ApproachRate: 9" ); | |
f.PutLine( "SliderMultiplier: 1.4" ); | |
f.PutLine( "SliderTickRate: 1" ); | |
f.PutLine(""); | |
RString bgp = out.GetBackgroundPath().substr( | |
out.GetBackgroundPath().find_last_of("/") + 1, // skip the / itself | |
out.GetBackgroundPath().length() - out.GetBackgroundPath().find_last_of("/") ); | |
f.PutLine("[Events]"); | |
f.PutLine("// Background and Video events"); | |
f.PutLine( ssprintf("0,0,\"%s\"", bgp.c_str() ) ); | |
f.PutLine( "// Break Periods"); | |
f.PutLine("// Storyboard Layer 0 (Background)"); | |
f.PutLine("// Storyboard Layer 1 (Fail)"); | |
f.PutLine("// Storyboard Layer 2 (Pass)"); | |
f.PutLine("// Storyboard Layer 3 (Foreground)"); | |
f.PutLine("// Storyboard Sound Samples"); | |
// todo: add autoplaykeysounds here | |
f.PutLine("// Background Colour Transformations"); | |
f.PutLine("3,100,163,162,255"); | |
f.PutLine(""); | |
f.Flush(); | |
} | |
static void WriteTimingData( RageFile &f, const Song &out, Steps* ToSave ) | |
{ | |
const TimingData &timing = ToSave->m_Timing; | |
f.PutLine("[TimingPoints]"); | |
// I assume these are sorted. It wouldn't be hard to sort them myself given the need. | |
vector <TimingSegment*> segments = timing.GetTimingSegments(SEGMENT_BPM); | |
vector <TimingSegment*> stopsegments = timing.GetTimingSegments(SEGMENT_STOP); | |
vector <TimingSegment*> scrollsegments = timing.GetTimingSegments(SEGMENT_SCROLL); | |
for (int i = 0; i < segments.size(); i++) | |
{ | |
float time = timing.GetElapsedTimeFromBeat(segments.at(i)->GetBeat()) * 1000; | |
float beatspace = 1000 / ((BPMSegment*)segments.at(i))->GetBPS(); | |
f.PutLine( ssprintf("%.0f,%.4f,4,1,0,15,1,0\n", | |
time, // time for this section | |
beatspace) | |
); // 1000 / bps = ms per beat, what we need right here. | |
} | |
// it's kinda stupid to use stops if you can use speeds, assuming you're charting for osu!mania. | |
// i don't recommend it, so restoring speed probably won't happen soon. | |
for (int i = 0; i < stopsegments.size(); i++) | |
{ | |
float time = timing.GetElapsedTimeFromBeat(stopsegments.at(i)->GetBeat()) * 1000; | |
float sectionmultiplier = -100 / 0.1; // lol maths | |
f.PutLine( ssprintf("%.0f,%.4f,4,1,0,15,0,0", | |
time, | |
sectionmultiplier) ); | |
// todo: check speeds and restore that. | |
float stoptimefinish = time + ((StopSegment*)stopsegments.at(i))->GetPause() * 1000; | |
f.PutLine( ssprintf("%.0f,%.4f,4,1,0,15,0,0", | |
stoptimefinish, | |
-100) ); | |
} | |
// speeds (recommended way to approach o!m charting anyway) minimum value supported by osu is 0.1 | |
// maximum is like 10 or something. | |
for (int i = 0; i < scrollsegments.size(); i++) | |
{ | |
float time = timing.GetElapsedTimeFromBeat(scrollsegments.at(i)->GetBeat()) * 1000; | |
float sectionmultiplier = -100 / ((ScrollSegment*)scrollsegments.at(i))->GetRatio(); // lol maths, again | |
// it's kinda stupid that osu uses -100/x instead of x/-100 but hell if i know. it doesn't really matter. | |
f.PutLine( ssprintf("%.0f,%.4f,4,1,0,15,0,0", | |
time, | |
sectionmultiplier) ); | |
} | |
f.PutLine(""); | |
f.Flush(); | |
} | |
static int TrackToXPos(int totaltracks, int track) | |
{ | |
double base = (512.0/totaltracks); | |
double minus = (256.0/totaltracks); | |
return (base * (track + 1) - minus); | |
} | |
static void WriteSteps( RageFile &f, const Song &out, Steps* ToSave ) | |
{ | |
vector<NoteData> parts; | |
float fLastBeat = -1.0f; | |
const TimingData &timing = ToSave->m_Timing; | |
float PendingTracks[16]; // For hold notes. | |
for (int i = 0; i < 16; i++) | |
{ | |
PendingTracks[i] = 0; | |
} | |
NoteDataUtil::SplitCompositeNoteData( ToSave->GetNoteData(), parts ); | |
FOREACH( NoteData, parts, nd ) | |
{ | |
NoteDataUtil::InsertHoldTails( *nd ); | |
fLastBeat = max( fLastBeat, nd->GetLastBeat() ); | |
} | |
int iLastMeasure = int( fLastBeat/4 ); | |
f.PutLine("[HitObjects]"); | |
FOREACH( NoteData, parts, nd ) | |
{ | |
for( int m = 0; m <= iLastMeasure; ++m ) // foreach measure | |
{ | |
NoteType nt = NoteDataUtil::GetSmallestNoteTypeForMeasure( *nd, m ); | |
int iRowSpacing; | |
if( nt == NoteType_Invalid ) | |
iRowSpacing = 1; | |
else | |
iRowSpacing = lrintf( NoteTypeToBeat(nt) * ROWS_PER_BEAT ); | |
const int iMeasureStartRow = m * 192; | |
const int iMeasureLastRow = (m+1) * 192 - 1; | |
for( int r=iMeasureStartRow; r<=iMeasureLastRow; r+=iRowSpacing ) | |
{ | |
for( int t = 0; t < nd->GetNumTracks(); ++t ) | |
{ | |
const TapNote &tn = nd->GetTapNote(t, r); | |
char c; | |
int xpos = TrackToXPos(nd->GetNumTracks(), t); | |
float ftiming = timing.GetElapsedTimeFromBeat(NoteRowToBeat(r)) * 1000; | |
switch( tn.type ) | |
{ | |
case TapNote::tap: // x, y, time, notetype, hitsound, whatever whatever whatever. | |
f.PutLine( ssprintf("%i,192,%.0f,1,0,0:0:0", xpos, ftiming) ); | |
break; | |
case TapNote::hold_head: | |
PendingTracks[t] = timing.GetElapsedTimeFromBeat(NoteRowToBeat(r)) * 1000; | |
break; | |
case TapNote::hold_tail: | |
f.PutLine( | |
ssprintf("%d,0,%.0f,128,0,%.0f,1:0:0\n", | |
TrackToXPos(nd->GetNumTracks(), t), | |
PendingTracks[t], | |
timing.GetElapsedTimeFromBeat(NoteRowToBeat(r)) * 1000 | |
)); | |
} // switch (tn.type) | |
} // for t=0.. | |
} // for r=0.. | |
} // for m = 0 | |
} // foreach.. | |
f.Flush(); | |
} | |
bool NotesWriterOSU::Write( const Song &out, const vector<Steps*>& vpStepsToSave ) | |
{ | |
int flags = RageFile::WRITE; | |
int totalSteps = vpStepsToSave.size(); | |
// ragefiles can't be vector'd :V -az | |
RageFile f; | |
for (int i = 0; i < totalSteps; i++) | |
{ | |
RString filename = out.GetSongDir() + out.GetDisplayArtist() + " - " + // directory/artist - title [difficulty].osu | |
out.GetDisplayMainTitle() + " [" + | |
vpStepsToSave.at(i)->GetChartName() + "].osu"; | |
if( !f.Open(filename, flags ) ) | |
{ | |
LOG->UserLog( "Song file", filename, "couldn't be opened for writing: %s", f.GetError().c_str() ); | |
return false; | |
} | |
WriteGlobalTags(f, out, vpStepsToSave.at(i)); | |
WriteTimingData(f, out, vpStepsToSave.at(i)); | |
WriteSteps(f, out, vpStepsToSave.at(i)); | |
f.Close(); | |
} | |
return true; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef NOTES_WRITER_OSU_H | |
#define NOTES_WRITER_OSU_H | |
class Song; | |
/** @brief Writes a Song to a .DWI file. */ | |
namespace NotesWriterOSU | |
{ | |
/** | |
* @brief Write the song out to a file. | |
* @param sPath the path to write the file. | |
* @param out the Song to be written out. | |
* @return its success or failure. */ | |
bool Write( const Song &out, const vector<Steps*>& vpStepsToSave ); | |
} | |
#endif |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment