diff --git a/Beatmap/include/Beatmap/Beatmap.hpp b/Beatmap/include/Beatmap/Beatmap.hpp index c63286886..345a22f0f 100644 --- a/Beatmap/include/Beatmap/Beatmap.hpp +++ b/Beatmap/include/Beatmap/Beatmap.hpp @@ -1,6 +1,7 @@ #pragma once #include "BeatmapObjects.hpp" #include "AudioEffects.hpp" +#include "EffectTimeline.hpp" /* Global settings stored in a beatmap */ struct BeatmapSettings @@ -57,66 +58,118 @@ struct BeatmapSettings class Beatmap : public Unique { public: - virtual ~Beatmap(); - Beatmap() = default; - Beatmap(Beatmap&& other); - Beatmap& operator=(Beatmap&& other); + // Vector interacts badly with unique_ptr, so std::vector was used instead. + using Objects = std::vector>; + using ObjectsIterator = Objects::const_iterator; + using TimingPoints = Vector; + using TimingPointsIterator = TimingPoints::const_iterator; + + using LaneTogglePoints = Vector; + using LaneTogglePointsIterator = LaneTogglePoints::const_iterator; + +public: bool Load(BinaryStream& input, bool metadataOnly = false); - // Saves the map as it's own format - bool Save(BinaryStream& output) const; - // Returns the settings of the map, contains metadata + song/image paths. + /// Returns the settings of the map, contains metadata + song/image paths. const BeatmapSettings& GetMapSettings() const; - // Vector of timing points in the map, sorted by when they appear in the map - // Must keep the beatmap class instance alive for these to stay valid - // Can contain multiple objects at the same time - const Vector& GetLinearTimingPoints() const; - // Vector of chart stops in the chart, sorted by when they appear in the map - // Must keep the beatmap class instance alive for these to stay valid - // Can contain multiple objects at the same time - const Vector& GetLinearChartStops() const; - // Vector of objects in the map, sorted by when they appear in the map - // Must keep the beatmap class instance alive for these to stay valid - // Can contain multiple objects at the same time - const Vector& GetLinearObjects() const; - // Vector of zoom control points in the map, sorted by when they appear in the map - // Must keep the beatmap class instance alive for these to stay valid - // Can contain multiple objects at the same time - const Vector& GetZoomControlPoints() const; - - const Vector& GetLaneTogglePoints() const; - - const Vector& GetSamplePaths() const; - - const Vector& GetSwitchablePaths() const; - - // Retrieves audio effect settings for a given button id + const Vector& GetLaneTogglePoints() const { return m_laneTogglePoints; } + + const Vector& GetSamplePaths() const { return m_samplePaths; } + const Vector& GetSwitchablePaths() const { return m_switchablePaths; } + + /// Retrieves audio effect settings for a given button id AudioEffect GetEffect(EffectType type) const; - // Retrieves audio effect settings for a given filter effect id + /// Retrieves audio effect settings for a given filter effect id AudioEffect GetFilter(EffectType type) const; - // Get the timing of the last (non-event) object + /// Get the timing of the first (non-event) object + MapTime GetFirstObjectTime(MapTime lowerBound) const; + /// Get the timing of the last (non-event) object MapTime GetLastObjectTime() const; + /// Get the timing of the last object, including the event objects + MapTime GetLastObjectTimeIncludingEvents() const; - // Measure -> Time + /// Measure -> Time MapTime GetMapTimeFromMeasureInd(int measure) const; - // Time -> Measure + /// Time -> Measure int GetMeasureIndFromMapTime(MapTime time) const; + /// Computes the most frequently occuring BPM (to be used for MMod) + double GetModeBPM() const; + void GetBPMInfo(double& startBPM, double& minBPM, double& maxBPM, double& modeBPM) const; + + void Shuffle(int seed, bool random, bool mirror); + void ApplyShuffle(const std::array& swaps, bool flipLaser); + + /// # of (4th-note) beats between the start and the end + float GetBeatCount(MapTime start, MapTime end, TimingPointsIterator hint) const; + float GetBeatCountWithScrollSpeedApplied(MapTime start, MapTime end, TimingPointsIterator hint) const; + + inline float GetBeatCount(MapTime start, MapTime end) const + { + return GetBeatCount(start, end, GetTimingPoint(start)); + } + + inline float GetBeatCountWithScrollSpeedApplied(MapTime start, MapTime end) const + { + return GetBeatCountWithScrollSpeedApplied(start, end, GetTimingPoint(start)); + } + + const Objects& GetObjectStates() const { return m_objectStates; } + + ObjectsIterator GetFirstObjectState() const { return m_objectStates.begin(); } + ObjectsIterator GetEndObjectState() const { return m_objectStates.end(); } + + bool HasObjectState() const { return !m_objectStates.empty(); } + + const TimingPoints& GetTimingPoints() const { return m_timingPoints; } + + TimingPointsIterator GetFirstTimingPoint() const { return m_timingPoints.begin(); } + TimingPointsIterator GetEndTimingPoint() const { return m_timingPoints.end(); } + + /// Returns the latest timing point for given mapTime + inline TimingPointsIterator GetTimingPoint(MapTime mapTime) const + { + return GetTimingPoint(mapTime, 0, m_timingPoints.size()); + } + + /// GetTimingPoint but a hint is given + TimingPointsIterator GetTimingPoint(MapTime mapTime, TimingPointsIterator hint, bool forwardOnly = false) const; + + /// GetTimingPoint but begin and end ranges are specified + inline TimingPointsIterator GetTimingPoint(MapTime mapTime, TimingPointsIterator beginIt, TimingPointsIterator endIt) const + { + return GetTimingPoint(mapTime, static_cast(std::distance(m_timingPoints.begin(), beginIt)), static_cast(std::distance(m_timingPoints.begin(), endIt))); + } + + TimingPointsIterator GetTimingPoint(MapTime mapTime, size_t begin, size_t end) const; + + LaneTogglePointsIterator GetFirstLaneTogglePoint() const { return m_laneTogglePoints.begin(); } + LaneTogglePointsIterator GetEndLaneTogglePoint() const { return m_laneTogglePoints.end(); } + + float GetGraphValueAt(EffectTimeline::GraphType type, MapTime mapTime) const; + bool CheckIfManualTiltInstant(MapTime bound, MapTime mapTime) const; + + float GetCenterSplitValueAt(MapTime mapTime) const; + float GetScrollSpeedAt(MapTime mapTime) const; + private: bool m_ProcessKShootMap(BinaryStream& input, bool metadataOnly); - bool m_Serialize(BinaryStream& stream, bool metadataOnly); - Map m_customEffects; - Map m_customFilters; + Map m_customAudioEffects; + Map m_customAudioFilters; + + Objects m_objectStates; + TimingPoints m_timingPoints; + + EffectTimeline m_effects; + + LineGraph m_centerSplit; + Vector m_laneTogglePoints; + Map> m_positionalOptions; - Vector m_timingPoints; - Vector m_chartStops; - Vector m_laneTogglePoints; - Vector m_objectStates; - Vector m_zoomControlPoints; Vector m_samplePaths; Vector m_switchablePaths; BeatmapSettings m_settings; diff --git a/Beatmap/include/Beatmap/BeatmapObjects.hpp b/Beatmap/include/Beatmap/BeatmapObjects.hpp index 5368c64f0..097db97c1 100644 --- a/Beatmap/include/Beatmap/BeatmapObjects.hpp +++ b/Beatmap/include/Beatmap/BeatmapObjects.hpp @@ -112,7 +112,7 @@ struct TObjectState : public ObjectTypeData_Base TObjectState() : ObjectTypeData_Base(ObjectType::Invalid){}; // Sort object states by their time and other properties - static void SortArray(Vector *> &arr); + static void SortArray(std::vector>>& arr); // Always allow casting from typeless object to Union State object operator MultiObjectState *() { return (MultiObjectState *)this; } @@ -180,9 +180,14 @@ struct SpinStruct struct ObjectTypeData_Laser { // Retrieves the starting laser point - TObjectState *GetRoot(); + TObjectState* GetRoot(); + inline const TObjectState* GetRoot() const + { + return const_cast(this)->GetRoot(); + } + // Ending point of laser - TObjectState *GetTail(); + TObjectState* GetTail(); float GetDirection() const; float SamplePosition(MapTime time) const; // Convert extended range to normal range @@ -300,50 +305,22 @@ struct TimingPoint double GetBarDuration() const { return GetWholeNoteLength() * ((double)numerator / (double)denominator); } double GetBPM() const { return 60000.0 / beatDuration; } - // Position in ms when this timing point appears + /// Position in ms when this timing point appears MapTime time = 0; - // Beat duration of a 4th note in milliseconds - // this is a double so the least precision is lost - // can be cast back to integer format once is has been multiplied by the amount of beats you want the length of. - // Calculated by taking (60000.0 / BPM) + /// Beat duration of a 4th note in milliseconds (equals 60000.0 / BPM) double beatDuration; - // Upper part of the time signature - // how many beats per bar + /// Upper part of the time signature (how many beats per bar) uint8 numerator = 4; - // Lower part of the time signature - // the note value (4th, 3th, 8th notes, etc.) for a beat + /// Lower part of the time signature (the note value (4th, 3th, 8th notes, etc.) for a beat) uint8 denominator = 4; + /// Multiplier for tickrates (x 2^tickrateOffset) int8 tickrateOffset = 0; }; -struct LaneHideTogglePoint -{ - // Position in ms when to hide or show the lane +struct LaneHideTogglePoint { + /// Position in ms when to hide or show the lane MapTime time; - // How long the transition to/from hidden should take in 1/192nd notes + /// How long the transition to/from hidden should take in 1/192nd notes uint32 duration = 192; -}; - -// Control point for track zoom levels -struct ZoomControlPoint -{ - MapTime time; - // What zoom to control - // 0 = bottom - // 1 = top - uint8 index = 0; - // The zoom value - // in the range -1 to 1 - // 1 being fully zoomed in - float zoom = 0.0f; - // Used to check if a manual tilt assignment is instant - bool instant = false; -}; - -// Chart stop object -struct ChartStop -{ - MapTime time; - MapTime duration; -}; +}; \ No newline at end of file diff --git a/Beatmap/include/Beatmap/BeatmapPlayback.hpp b/Beatmap/include/Beatmap/BeatmapPlayback.hpp index 72de40b77..5f860fca6 100644 --- a/Beatmap/include/Beatmap/BeatmapPlayback.hpp +++ b/Beatmap/include/Beatmap/BeatmapPlayback.hpp @@ -8,8 +8,7 @@ class BeatmapPlayback { public: BeatmapPlayback() = default; - BeatmapPlayback(Beatmap& beatmap); - ~BeatmapPlayback(); + BeatmapPlayback(const Beatmap& beatmap); // Resets the playback of the map // Must be called before any other function is called on this object @@ -32,10 +31,12 @@ class BeatmapPlayback // Removes any existing data and sets a special behaviour for calibration mode void MakeCalibrationPlayback(); - // Gets all linear objects that fall within the given time range: - // - Vector GetObjectsInRange(MapTime range); - ObjectState* GetFirstButtonOrHoldAfterTime(MapTime t, int lane); + /// Get all objects that fall within the given visible range, + /// `numBeats` is the # of 4th notes + void GetObjectsInViewRange(float numBeats, Vector& objects); + void GetBarPositionsInViewRange(float numBeats, Vector& barPositions) const; + + const ObjectState* GetFirstButtonOrHoldAfterTime(MapTime t, int lane) const; // Duration for objects to keep being returned by GetObjectsInRange after they have passed the current time MapTime keepObjectDuration = 1000; @@ -56,32 +57,52 @@ class BeatmapPlayback uint32 CountBeats(MapTime start, MapTime range, int32& startIndex, uint32 multiplier = 1) const; // View coordinate conversions - // the input duration is looped throught the timing points that affect it and the resulting float is the number of 4th note offets - MapTime ViewDistanceToDuration(float distance); - float DurationToViewDistance(MapTime time); - float DurationToViewDistanceAtTime(MapTime time, MapTime duration); - float DurationToViewDistanceAtTimeNoStops(MapTime time, MapTime duration); - float TimeToViewDistance(MapTime time); + inline float TimeToViewDistance(MapTime mapTime) const + { + return GetViewDistance(m_playbackTime, mapTime); + } + + inline float TimeToViewDistanceIgnoringScrollSpeed(MapTime mapTime) const + { + return GetViewDistanceIgnoringScrollSpeed(m_playbackTime, mapTime); + } + + inline float ToViewDistance(MapTime startTime, MapTime duration) const + { + return GetViewDistance(startTime, startTime + duration); + } + + inline float ToViewDistanceIgnoringScrollSpeed(MapTime startTime, MapTime duration) const + { + return GetViewDistanceIgnoringScrollSpeed(startTime, startTime + duration); + } + + /// Get # of (4th) beats between `startTime` and `endTime`, taking scroll speed changes into account. + float GetViewDistance(MapTime startTime, MapTime endTime) const; + + /// Get # of (4th) beats between `startTime` and `endTime`, ignoring any scroll speed changes. + float GetViewDistanceIgnoringScrollSpeed(MapTime startTime, MapTime endTime) const; // Current map time in ms as last passed to Update - MapTime GetLastTime() const; + inline MapTime GetLastTime() const { return m_playbackTime; } // Value from 0 to 1 that indicates how far in a single bar the playback is - float GetBarTime() const; - float GetBeatTime() const; + inline float GetBarTime() const { return m_barTime; } + inline float GetBeatTime() const { return m_beatTime; } // Gets the currently set value of a value set by events in the beatmap const EventData& GetEventData(EventKey key); + // Retrieve event data as any 32-bit type template - const T& GetEventData(EventKey key) + const typename std::enable_if::value && sizeof(T) <= 4, T>::type& GetEventData(EventKey key) { - assert(sizeof(T) <= 4); return *(T*)&GetEventData(key); } // Get interpolated top or bottom zoom as set by the map - float GetZoom(uint8 index); + float GetZoom(uint8 index) const; + float GetScrollSpeed() const; // Checks if current manual tilt value is instant bool CheckIfManualTiltInstant(); @@ -99,55 +120,46 @@ class BeatmapPlayback Delegate OnFXEnd; // Called when a new timing point becomes active - Delegate OnTimingPointChanged; - - Delegate OnLaneToggleChanged; + Delegate OnTimingPointChanged; + Delegate OnLaneToggleChanged; Delegate OnEventChanged; private: // Selects an object or timing point based on a given input state // if allowReset is true the search starts from the start of the object list if current point lies beyond given input time - TimingPoint** m_SelectTimingPoint(MapTime time, bool allowReset = false); - LaneHideTogglePoint** m_SelectLaneTogglePoint(MapTime time, bool allowReset = false); - ObjectState** m_SelectHitObject(MapTime time, bool allowReset = false); - ZoomControlPoint** m_SelectZoomObject(MapTime time); - Vector m_SelectChartStops(MapTime time, MapTime duration); - - // End object pointer, this is not a valid pointer, but points to the element after the last element - bool IsEndTiming(TimingPoint** obj); - bool IsEndObject(ObjectState** obj); - bool IsEndLaneToggle(LaneHideTogglePoint ** obj); - bool IsEndZoomPoint(ZoomControlPoint** obj); + Beatmap::ObjectsIterator m_SelectHitObject(MapTime time, bool allowReset = false) const; + Beatmap::TimingPointsIterator m_SelectTimingPoint(MapTime time, bool allowReset = false) const; + Beatmap::LaneTogglePointsIterator m_SelectLaneTogglePoint(MapTime time, bool allowReset = false) const; + + // End object iterator, this is not a valid iterator, but points to the element after the last element + bool IsEndObject(const Beatmap::ObjectsIterator& obj) const; + bool IsEndTiming(const Beatmap::TimingPointsIterator& obj) const; + bool IsEndLaneToggle(const Beatmap::LaneTogglePointsIterator& obj) const; // Current map position of this playback object MapTime m_playbackTime; - // Disregard objects outside of these ranges - MapTimeRange m_viewRange; - - Vector m_timingPoints; - Vector m_chartStops; - Vector m_objects; - Vector m_zoomPoints; - Vector m_laneTogglePoints; + // Disregard objects outside of this range + MapTimeRange m_playRange; bool m_initialEffectStateSent = false; - TimingPoint** m_currentTiming = nullptr; - ObjectState** m_currentObj = nullptr; - ObjectState** m_currentLaserObj = nullptr; - ObjectState** m_currentAlertObj = nullptr; - LaneHideTogglePoint** m_currentLaneTogglePoint = nullptr; - ZoomControlPoint** m_currentZoomPoint = nullptr; + Beatmap::ObjectsIterator m_currObject; + Beatmap::ObjectsIterator m_currLaserObject; + Beatmap::ObjectsIterator m_currAlertObject; + + Beatmap::TimingPointsIterator m_currentTiming; + Beatmap::LaneTogglePointsIterator m_currentLaneTogglePoint; - // Used to calculate track zoom - ZoomControlPoint* m_zoomStartPoints[5] = { nullptr }; - ZoomControlPoint* m_zoomEndPoints[5] = { nullptr }; + TrackRollBehaviour m_currentTrackRollBehaviour = TrackRollBehaviour::Normal; + MapTime m_lastTrackRollBehaviourChange = 0; // Contains all the objects that are in the current valid timing area - Vector m_hittableObjects; - // Hold objects to render even when their start time is not in the current visibility range - Set m_holdObjects; + Multimap m_objectsByTime; + + // Ordered by leaving time + Multimap m_objectsByLeaveTime; + // Hold buttons with effects that are active Set m_effectObjects; @@ -157,9 +169,10 @@ class BeatmapPlayback float m_barTime; float m_beatTime; - Beatmap* m_beatmap = nullptr; + const Beatmap* m_beatmap = nullptr; - //calibration mode things + // For the calibration mode bool m_isCalibration = false; - Vector m_calibrationObjects; + Vector> m_calibrationObjects; + TimingPoint m_calibrationTiming; }; \ No newline at end of file diff --git a/Beatmap/include/Beatmap/EffectTimeline.hpp b/Beatmap/include/Beatmap/EffectTimeline.hpp new file mode 100644 index 000000000..588484f9d --- /dev/null +++ b/Beatmap/include/Beatmap/EffectTimeline.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "BeatmapObjects.hpp" +#include "LineGraph.hpp" + +/// Loosely following https://github.com/m4saka/ksh + +/// Contains camera and other miscellaneous effects for lane / notes +class EffectTimeline { +public: + enum class GraphType { + ZOOM_BOTTOM, + ZOOM_TOP, + SHIFT_X, + ROTATION_Z, + SCROLL_SPEED, + }; + + inline LineGraph& GetGraph(GraphType type) + { + switch (type) { + case GraphType::ZOOM_BOTTOM: return m_zoomBottom; + case GraphType::ZOOM_TOP: return m_zoomTop; + case GraphType::SHIFT_X: return m_shiftX; + case GraphType::ROTATION_Z: return m_rotationZ; + case GraphType::SCROLL_SPEED: return m_scrollSpeed; + + // Shouldn't happen at all. + default: assert(false); return m_shiftX; + } + } + + inline const LineGraph& GetGraph(GraphType type) const + { + switch (type) { + case GraphType::ZOOM_BOTTOM: return m_zoomBottom; + case GraphType::ZOOM_TOP: return m_zoomTop; + case GraphType::SHIFT_X: return m_shiftX; + case GraphType::ROTATION_Z: return m_rotationZ; + case GraphType::SCROLL_SPEED: return m_scrollSpeed; + + // Shouldn't happen at all. + default: assert(false); return m_shiftX; + } + } + + inline void InsertGraphValue(GraphType type, MapTime mapTime, double value) + { + GetGraph(type).Insert(mapTime, value); + } + +private: + LineGraph m_zoomBottom; + LineGraph m_zoomTop; + LineGraph m_shiftX; /// former zoom_side + + LineGraph m_rotationZ; /// former manual tilt + + LineGraph m_scrollSpeed = LineGraph{1.0}; +}; \ No newline at end of file diff --git a/Beatmap/include/Beatmap/LineGraph.hpp b/Beatmap/include/Beatmap/LineGraph.hpp new file mode 100644 index 000000000..786975df7 --- /dev/null +++ b/Beatmap/include/Beatmap/LineGraph.hpp @@ -0,0 +1,115 @@ +#pragma once + +/// From https://github.com/m4saka/ksh +/// but modified to USC's taste and somewhat compatible to KSON + +#include +#include + +#include "BeatmapObjects.hpp" + +class LineGraph { +public: + LineGraph(double defaultValue = 0.0) : m_default(defaultValue) {} + + struct Point { + explicit Point(double val) : value(val, val) {} + explicit Point(double start, double end) : value(start, end) {} + + inline bool IsSlam() const { return value.first != value.second; } + + std::pair value; + std::pair curve = {}; + }; + +private: + using Points = Map; + Points m_points; + const double m_default = 0.0; + +public: + using PointsIterator = Points::const_iterator; + + void Insert(MapTime time, double point); + void Insert(MapTime time, const Point& point); + void Insert(MapTime time, const std::string& point); + + void RangeSet(MapTime begin, MapTime end, double value); + void RangeAdd(MapTime begin, MapTime end, double delta); + + /// Returns the value being extended. + double Extend(MapTime time); + + double Integrate(MapTime begin, MapTime end) const; + + /// When you know for certain that curr->first <= begin <= end <= std::next(curr)->first + double Integrate(PointsIterator curr, MapTime begin, MapTime end) const; + double Integrate(PointsIterator curr) const; + + inline PointsIterator lower_bound(MapTime time) const + { + return m_points.lower_bound(time); + } + + inline PointsIterator upper_bound(MapTime time) const + { + return m_points.upper_bound(time); + } + + inline std::size_t erase(MapTime time) + { + return m_points.erase(time); + } + + inline const Point& at(MapTime time) const + { + return m_points.at(time); + } + + inline Map::iterator begin() + { + return m_points.begin(); + } + + inline PointsIterator begin() const + { + return m_points.begin(); + } + + inline PointsIterator cbegin() const + { + return m_points.cbegin(); + } + + inline Map::iterator end() + { + return m_points.end(); + } + + inline PointsIterator end() const + { + return m_points.end(); + } + + inline PointsIterator cend() const + { + return m_points.cend(); + } + + inline std::size_t size() const + { + return m_points.size(); + } + + inline bool empty() const + { + return m_points.empty(); + } + + inline std::size_t count(MapTime mapTime) const + { + return m_points.count(mapTime); + } + + double ValueAt(MapTime mapTime) const; +}; \ No newline at end of file diff --git a/Beatmap/src/Beatmap.cpp b/Beatmap/src/Beatmap.cpp index 10f01bb4b..18efbbc26 100644 --- a/Beatmap/src/Beatmap.cpp +++ b/Beatmap/src/Beatmap.cpp @@ -2,65 +2,16 @@ #include "Beatmap.hpp" #include "Shared/Profiling.hpp" +#include +#include + static const uint32 c_mapVersion = 1; -Beatmap::~Beatmap() -{ - // Perform cleanup - for(auto tp : m_timingPoints) - delete tp; - for(auto obj : m_objectStates) - delete obj; - for (auto z : m_zoomControlPoints) - delete z; - for (auto z : m_laneTogglePoints) - delete z; - for (auto cs : m_chartStops) - delete cs; -} -Beatmap::Beatmap(Beatmap&& other) -{ - m_timingPoints = std::move(other.m_timingPoints); - m_objectStates = std::move(other.m_objectStates); - m_zoomControlPoints = std::move(other.m_zoomControlPoints); - m_laneTogglePoints = std::move(other.m_laneTogglePoints); - m_settings = std::move(other.m_settings); -} -Beatmap& Beatmap::operator=(Beatmap&& other) -{ - // Perform cleanup - for(auto tp : m_timingPoints) - delete tp; - for(auto obj : m_objectStates) - delete obj; - for(auto z : m_zoomControlPoints) - delete z; - m_timingPoints = std::move(other.m_timingPoints); - m_objectStates = std::move(other.m_objectStates); - m_zoomControlPoints = std::move(other.m_zoomControlPoints); - m_laneTogglePoints = std::move(other.m_laneTogglePoints); - m_settings = std::move(other.m_settings); - return *this; -} bool Beatmap::Load(BinaryStream& input, bool metadataOnly) { ProfilerScope $("Load Beatmap"); - if(!m_ProcessKShootMap(input, metadataOnly)) // Load KSH format first - { - // Load binary map format - input.Seek(0); - if(!m_Serialize(input, metadataOnly)) - return false; - } - - return true; -} -bool Beatmap::Save(BinaryStream& output) const -{ - ProfilerScope $("Save Beatmap"); - // Const cast because serialize is universal for loading and saving - return const_cast(this)->m_Serialize(output, false); + return m_ProcessKShootMap(input, metadataOnly); } const BeatmapSettings& Beatmap::GetMapSettings() const @@ -68,74 +19,64 @@ const BeatmapSettings& Beatmap::GetMapSettings() const return m_settings; } -const Vector& Beatmap::GetLinearTimingPoints() const -{ - return m_timingPoints; -} -const Vector& Beatmap::GetLinearChartStops() const -{ - return m_chartStops; -} -const Vector& Beatmap::GetLaneTogglePoints() const -{ - return m_laneTogglePoints; -} -const Vector& Beatmap::GetLinearObjects() const -{ - return reinterpret_cast&>(m_objectStates); -} -const Vector& Beatmap::GetZoomControlPoints() const -{ - return m_zoomControlPoints; -} - -const Vector& Beatmap::GetSamplePaths() const -{ - return m_samplePaths; -} - -const Vector& Beatmap::GetSwitchablePaths() const -{ - return m_switchablePaths; -} - AudioEffect Beatmap::GetEffect(EffectType type) const { if(type >= EffectType::UserDefined0) { - const AudioEffect* fx = m_customEffects.Find(type); + const AudioEffect* fx = m_customAudioEffects.Find(type); assert(fx); return *fx; } return AudioEffect::GetDefault(type); } + AudioEffect Beatmap::GetFilter(EffectType type) const { if(type >= EffectType::UserDefined0) { - const AudioEffect* fx = m_customFilters.Find(type); + const AudioEffect* fx = m_customAudioFilters.Find(type); assert(fx); return *fx; } return AudioEffect::GetDefault(type); } +MapTime Beatmap::GetFirstObjectTime(MapTime lowerBound) const +{ + if (m_objectStates.empty()) + { + return lowerBound; + } + + for (const auto& obj : m_objectStates) + { + if (obj->type == ObjectType::Event) continue; + if (obj->time < lowerBound) continue; + + return obj->time; + } + + return lowerBound; +} + MapTime Beatmap::GetLastObjectTime() const { - if (m_objectStates.size() == 0) + if (m_objectStates.empty()) + { return 0; + } for (auto it = m_objectStates.rbegin(); it != m_objectStates.rend(); ++it) { - const ObjectState* obj = *it; + const auto& obj = *it; switch (obj->type) { case ObjectType::Event: continue; case ObjectType::Hold: - return obj->time + ((const HoldObjectState*)obj)->duration; + return obj->time + ((const HoldObjectState*) obj.get())->duration; case ObjectType::Laser: - return obj->time + ((const LaserObjectState*)obj)->duration; + return obj->time + ((const LaserObjectState*) obj.get())->duration; default: return obj->time; } @@ -144,17 +85,22 @@ MapTime Beatmap::GetLastObjectTime() const return 0; } +MapTime Beatmap::GetLastObjectTimeIncludingEvents() const +{ + return m_objectStates.empty() ? 0 : m_objectStates.back()->time; +} + constexpr static double MEASURE_EPSILON = 0.005; -inline static int GetBarCount(const TimingPoint* a, const TimingPoint* b) +inline static int GetBarCount(const TimingPoint& a, const TimingPoint& b) { - const MapTime measureDuration = b->time - a->time; - const double barCount = measureDuration / a->GetBarDuration(); + const MapTime measureDuration = b.time - a.time; + const double barCount = measureDuration / a.GetBarDuration(); int barCountInt = static_cast(barCount + 0.5); if (std::abs(barCount - static_cast(barCountInt)) >= MEASURE_EPSILON) { - Logf("A timing point at %d contains non-integer # of bars: %g", Logger::Severity::Info, a->time, barCount); + Logf("A timing point at %d contains non-integer # of bars: %g", Logger::Severity::Debug, a.time, barCount); if (barCount > barCountInt) ++barCountInt; } @@ -185,7 +131,7 @@ MapTime Beatmap::GetMapTimeFromMeasureInd(int measure) const if (isInCurrentTimingPoint) { measure -= currMeasure; - return static_cast(m_timingPoints[i]->time + m_timingPoints[i]->GetBarDuration() * measure); + return static_cast(m_timingPoints[i].time + m_timingPoints[i].GetBarDuration() * measure); } } @@ -200,86 +146,421 @@ int Beatmap::GetMeasureIndFromMapTime(MapTime time) const int currMeasureCount = 0; for (int i = 0; i < m_timingPoints.size(); ++i) { - if (i < m_timingPoints.size() - 1 && m_timingPoints[i + 1]->time <= time) + if (i < m_timingPoints.size() - 1 && m_timingPoints[i + 1].time <= time) { currMeasureCount += GetBarCount(m_timingPoints[i], m_timingPoints[i + 1]); continue; } - return currMeasureCount + static_cast(MEASURE_EPSILON + (time - m_timingPoints[i]->time) / m_timingPoints[i]->GetBarDuration()); + return currMeasureCount + static_cast(MEASURE_EPSILON + (time - m_timingPoints[i].time) / m_timingPoints[i].GetBarDuration()); } assert(false); return 0; } -bool MultiObjectState::StaticSerialize(BinaryStream& stream, MultiObjectState*& obj) +double Beatmap::GetModeBPM() const { - uint8 type = 0; - if(stream.IsReading()) + Map bpmDurations; + + MapTime lastMT = m_settings.offset; + double largestMT = -1; + double useBPM = -1; + double lastBPM = -1; + + for (const TimingPoint& tp : m_timingPoints) { - // Read type and create appropriate object - stream << type; - switch((ObjectType)type) + const double thisBPM = tp.GetBPM(); + const MapTime timeSinceLastTP = tp.time - lastMT; + + const double duration = bpmDurations[lastBPM] += timeSinceLastTP; + if (duration > largestMT) { - case ObjectType::Single: - obj = (MultiObjectState*)new ButtonObjectState(); - break; - case ObjectType::Hold: - obj = (MultiObjectState*)new HoldObjectState(); - break; - case ObjectType::Laser: - obj = (MultiObjectState*)new LaserObjectState(); - break; - case ObjectType::Event: - obj = (MultiObjectState*)new EventObjectState(); + useBPM = lastBPM; + largestMT = duration; + } + lastMT = tp.time; + lastBPM = thisBPM; + } + + bpmDurations[lastBPM] += GetLastObjectTime() - lastMT; + + if (bpmDurations[lastBPM] > largestMT) + { + useBPM = lastBPM; + } + + return useBPM; +} + +void Beatmap::GetBPMInfo(double& startBPM, double& minBPM, double& maxBPM, double& modeBPM) const +{ + startBPM = -1; + minBPM = -1; + maxBPM = -1; + modeBPM = -1; + + Map bpmDurations; + + MapTime lastMT = m_settings.offset; + + double largestMT = -1; + double lastBPM = -1; + + for (const TimingPoint& tp : m_timingPoints) + { + const double thisBPM = tp.GetBPM(); + const MapTime timeSinceLastTP = tp.time - lastMT; + + if (startBPM == -1) startBPM = thisBPM; + if (minBPM == -1 || minBPM > thisBPM) minBPM = thisBPM; + if (maxBPM == -1 || maxBPM < thisBPM) maxBPM = thisBPM; + + const double duration = bpmDurations[lastBPM] += timeSinceLastTP; + if (duration > largestMT) + { + modeBPM = lastBPM; + largestMT = duration; + } + lastMT = tp.time; + lastBPM = thisBPM; + } + + bpmDurations[lastBPM] += GetLastObjectTime() - lastMT; + + if (bpmDurations[lastBPM] > largestMT) + { + modeBPM = lastBPM; + } +} + +void Beatmap::Shuffle(int seed, bool random, bool mirror) +{ + if (!random && !mirror) return; + + if (!random) + { + assert(mirror); + ApplyShuffle({3, 2, 1, 0, 5, 4}, true); + + return; + } + + std::default_random_engine engine(seed); + std::array swaps = {0, 1, 2, 3, 4, 5}; + + std::shuffle(swaps.begin(), swaps.begin() + 4, engine); + std::shuffle(swaps.begin() + 4, swaps.end(), engine); + + bool unchanged = true; + for (int i = 0; i < 4; ++i) + { + if (swaps[i] != (mirror ? 3 - i : i)) + { + unchanged = true; break; } } + + if (unchanged) + { + if (mirror) + { + swaps[4] = 4; + swaps[5] = 5; + } + else + { + swaps[4] = 5; + swaps[5] = 4; + } + } + + ApplyShuffle(swaps, mirror); +} + +void Beatmap::ApplyShuffle(const std::array& swaps, bool flipLaser) +{ + for (auto& object : m_objectStates) + { + if (object->type == ObjectType::Single || object->type == ObjectType::Hold) + { + ButtonObjectState* bos = (ButtonObjectState*)object.get(); + bos->index = swaps[bos->index]; + } + else if (object->type == ObjectType::Laser) + { + LaserObjectState* los = (LaserObjectState*)object.get(); + + if (flipLaser) + { + los->index = (los->index + 1) % 2; + for (size_t i = 0; i < 2; i++) + { + los->points[i] = fabsf(los->points[i] - 1.0f); + } + } + } + } +} + +float Beatmap::GetBeatCount(MapTime start, MapTime end, TimingPointsIterator hint) const +{ + int sign = 1; + + if (m_timingPoints.empty() || start == end) + { + return 0.0f; + } + + if (end < start) + { + std::swap(start, end); + sign = -1; + } + + TimingPointsIterator tp = GetTimingPoint(start, hint); + assert(tp != m_timingPoints.end()); + + TimingPointsIterator tp_next = std::next(tp); + + float result = 0.0f; + MapTime refTime = start; + + while (tp_next != m_timingPoints.end() && tp_next->time < end) + { + result += (tp_next->time - refTime) / tp->beatDuration; + + tp = tp_next; + tp_next = std::next(tp); + refTime = tp->time; + } + + result += static_cast((end - refTime) / tp->beatDuration); + + return sign * result; +} + +float Beatmap::GetBeatCountWithScrollSpeedApplied(MapTime start, MapTime end, TimingPointsIterator hint) const +{ + int sign = 1; + + if (m_timingPoints.empty() || start == end) + { + return 0.0f; + } + + if (end < start) + { + std::swap(start, end); + sign = -1; + } + + TimingPointsIterator tp = GetTimingPoint(start, hint); + assert(tp != m_timingPoints.end()); + + TimingPointsIterator tp_next = std::next(tp); + + float result = 0.0f; + MapTime refTime = start; + + const LineGraph& scrollSpeedGraph = m_effects.GetGraph(EffectTimeline::GraphType::SCROLL_SPEED); + + while (tp_next != m_timingPoints.end() && tp_next->time < end) + { + result += static_cast(scrollSpeedGraph.Integrate(refTime, tp_next->time) / tp->beatDuration); + + tp = tp_next; + tp_next = std::next(tp); + refTime = tp->time; + } + + result += static_cast(scrollSpeedGraph.Integrate(refTime, end) / tp->beatDuration); + + return sign * result; +} + +Beatmap::TimingPointsIterator Beatmap::GetTimingPoint(MapTime mapTime, TimingPointsIterator hint, bool forwardOnly) const +{ + if (m_timingPoints.empty()) + { + return m_timingPoints.end(); + } + + // Check for common cases + if (hint != m_timingPoints.end()) + { + if (hint->time <= mapTime) + { + if (std::next(hint) == m_timingPoints.end()) + { + return hint; + } + if (mapTime < std::next(hint)->time) + { + return hint; + } + else + { + ++hint; + } + } + else if (forwardOnly) + { + return hint; + } + else if (hint == m_timingPoints.begin()) + { + return hint; + } + else if (std::prev(hint)->time <= mapTime) + { + return std::prev(hint); + } + else + { + --hint; + } + } + + size_t hintInd = static_cast(std::distance(m_timingPoints.begin(), hint)); + + if (hint == m_timingPoints.end() || mapTime < hint->time) + { + // mapTime before hint + size_t diff = 1; + size_t prevDiff = 0; + while (diff <= hintInd) + { + if (m_timingPoints[hintInd - diff].time <= mapTime) + { + break; + } + + prevDiff = diff; + diff *= 2; + } + + if (diff > hintInd) + { + diff = hintInd; + } + return GetTimingPoint(mapTime, hintInd - diff, hintInd - prevDiff); + } + else if (hint->time == mapTime) + { + return hint; + } else { - // Write type - type = (uint8)obj->type; - stream << type; - } - - // Pointer is always initialized here, serialize data - stream << obj->time; // Time always set - switch(obj->type) - { - case ObjectType::Single: - stream << obj->button.index; - break; - case ObjectType::Hold: - stream << obj->hold.index; - stream << obj->hold.duration; - stream << (uint16&)obj->hold.effectType; - stream << (int16&)obj->hold.effectParams[0]; - stream << (int16&)obj->hold.effectParams[1]; - break; - case ObjectType::Laser: - stream << obj->laser.index; - stream << obj->laser.duration; - stream << obj->laser.points[0]; - stream << obj->laser.points[1]; - stream << obj->laser.flags; - break; - case ObjectType::Event: - stream << (uint8&)obj->event.key; - stream << *&obj->event.data; - break; + // mapTime after hint + size_t diff = 1; + size_t prevDiff = 0; + while (hintInd + diff < m_timingPoints.size()) + { + if (mapTime <= m_timingPoints[hintInd + diff].time) + { + break; + } + + prevDiff = diff; + diff *= 2; + } + + if (hintInd + diff >= m_timingPoints.size()) + { + diff = m_timingPoints.size() - 1 - hintInd; + } + + return GetTimingPoint(mapTime, hintInd + prevDiff, hintInd + diff + 1); } +} - return true; +Beatmap::TimingPointsIterator Beatmap::GetTimingPoint(MapTime mapTime, size_t begin, size_t end) const +{ + if (end <= begin) + { + return m_timingPoints.begin(); + } + + if (mapTime < m_timingPoints[begin].time) + { + return m_timingPoints.begin() + begin; + } + + if (end < m_timingPoints.size() && mapTime >= m_timingPoints[end].time) + { + return m_timingPoints.begin() + end; + } + + while (begin + 2 < end) + { + const size_t mid = (begin + end) / 2; + if (m_timingPoints[mid].time < mapTime) + { + begin = mid; + } + else if (m_timingPoints[mid].time > mapTime) + { + end = mid; + } + else + { + return m_timingPoints.begin() + mid; + } + } + + if (begin + 1 < end && m_timingPoints[begin + 1].time <= mapTime) + { + return m_timingPoints.begin() + (begin + 1); + } + else + { + return m_timingPoints.begin() + begin; + } } -bool TimingPoint::StaticSerialize(BinaryStream& stream, TimingPoint*& out) + +float Beatmap::GetGraphValueAt(EffectTimeline::GraphType type, MapTime mapTime) const { - if(stream.IsReading()) - out = new TimingPoint(); - stream << out->time; - stream << out->beatDuration; - stream << out->numerator; - return true; + return static_cast(m_effects.GetGraph(type).ValueAt(mapTime)); +} + +bool Beatmap::CheckIfManualTiltInstant(MapTime bound, MapTime mapTime) const +{ + auto checkManualTiltInstant = [&](const EffectTimeline& timeline) { + const LineGraph& graph = timeline.GetGraph(EffectTimeline::GraphType::ROTATION_Z); + if (graph.empty()) return false; + + const LineGraph::PointsIterator point = graph.upper_bound(bound); + if (point == graph.end()) + { + return false; + } + + if (!point->second.IsSlam()) + { + return false; + } + + if (point->first > mapTime) + { + return false; + } + + return true; + }; + + return checkManualTiltInstant(m_effects); +} + +float Beatmap::GetCenterSplitValueAt(MapTime mapTime) const +{ + return static_cast(m_centerSplit.ValueAt(mapTime)); +} + +float Beatmap::GetScrollSpeedAt(MapTime mapTime) const +{ + return static_cast(m_effects.GetGraph(EffectTimeline::GraphType::SCROLL_SPEED).ValueAt(mapTime)); } BinaryStream& operator<<(BinaryStream& stream, BeatmapSettings& settings) @@ -309,56 +590,6 @@ BinaryStream& operator<<(BinaryStream& stream, BeatmapSettings& settings) stream << (uint8&)settings.laserEffectType; return stream; } -bool Beatmap::m_Serialize(BinaryStream& stream, bool metadataOnly) -{ - static const uint32 c_magic = *(uint32*)"FXMM"; - uint32 magic = c_magic; - uint32 version = c_mapVersion; - stream << magic; - stream << version; - - // Validate headers when reading - if(stream.IsReading()) - { - if(magic != c_magic) - { - Log("Invalid map format", Logger::Severity::Warning); - return false; - } - if(version != c_mapVersion) - { - Logf("Incompatible map version [%d], loader is version %d", Logger::Severity::Warning, version, c_mapVersion); - return false; - } - } - - stream << m_settings; - stream << m_timingPoints; - stream << reinterpret_cast&>(m_objectStates); - - // Manually fix up laser next-prev pointers - LaserObjectState* prevLasers[2] = { 0 }; - if(stream.IsReading()) - { - for(ObjectState* obj : m_objectStates) - { - if(obj->type == ObjectType::Laser) - { - LaserObjectState* laser = (LaserObjectState*)obj; - LaserObjectState*& prev = prevLasers[laser->index]; - if(prev && (prev->time + prev->duration) == laser->time) - { - prev->next = laser; - laser->prev = prev; - } - - prev = laser; - } - } - } - - return true; -} bool BeatmapSettings::StaticSerialize(BinaryStream& stream, BeatmapSettings*& settings) { @@ -366,4 +597,4 @@ bool BeatmapSettings::StaticSerialize(BinaryStream& stream, BeatmapSettings*& se settings = new BeatmapSettings(); stream << *settings; return true; -} +} \ No newline at end of file diff --git a/Beatmap/src/BeatmapFromKSH.cpp b/Beatmap/src/BeatmapFromKSH.cpp index 2a6a1e900..3a2869016 100755 --- a/Beatmap/src/BeatmapFromKSH.cpp +++ b/Beatmap/src/BeatmapFromKSH.cpp @@ -24,12 +24,10 @@ struct TempButtonState }; struct TempLaserState { - TempLaserState(uint32 startTick, uint32 absoluteStartTick, uint32 effectType, TimingPoint *tpStart) - : startTick(startTick), effectType(effectType), tpStart(tpStart), absoluteStartTick(absoluteStartTick) + TempLaserState(uint32 startTick, uint32 absoluteStartTick, uint32 effectType) + : startTick(startTick), effectType(effectType), absoluteStartTick(absoluteStartTick) { } - // Timing point at which this segment started - TimingPoint *tpStart; uint32 startTick; uint32 absoluteStartTick; uint32 numTicks = 0; @@ -108,29 +106,6 @@ void AssignAudioEffectParameter(EffectParam ¶m, const String ¶mName, } } -double TimeFromTicks(uint32 tick, const Map &timingpoints, double resolution) -{ - TimingPoint *lastTp = timingpoints.begin()->second; - uint32 lastTick = timingpoints.begin()->first; - double ret = lastTp->time; - for (auto kvp : timingpoints) - { - if (kvp.first > tick) - { - break; - } - ret += Math::MSFromTicks((double)(kvp.first - lastTick), lastTp->GetBPM(), resolution); - lastTp = kvp.second; - lastTick = kvp.first; - } - return ret + Math::MSFromTicks((double)(tick - lastTick), lastTp->GetBPM(), resolution); -} - -MapTime MapTimeFromTicks(uint32 tick, const Map &timingpoints, double resolution) -{ - return Math::Round(TimeFromTicks(tick, timingpoints, resolution)); -} - struct MultiParam { enum Type @@ -228,7 +203,7 @@ static MultiParam ParseParam(const String &in) ret.type = MultiParam::Float; int percentage = 0; sscanf(*in, "%i", &percentage); - ret.fval = percentage / 100.0; + ret.fval = percentage / 100.0f; } else if (in.find('.') != -1) { @@ -275,12 +250,12 @@ AudioEffect ParseCustomEffect(const KShootEffectDefinition &def, Vector auto it = std::find(switchablePaths.begin(), switchablePaths.end(), s.second); if (it == switchablePaths.end()) { - switchableIndex.ival = switchablePaths.size(); + switchableIndex.ival = static_cast(switchablePaths.size()); switchablePaths.Add(s.second); } else { - switchableIndex.ival = std::distance(switchablePaths.begin(), it); + switchableIndex.ival = static_cast(std::distance(switchablePaths.begin(), it)); } params.Add("index", switchableIndex); @@ -447,16 +422,16 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) for (auto it = kshootMap.fxDefines.begin(); it != kshootMap.fxDefines.end(); it++) { EffectType type = effectTypeMap.FindOrAddEffectType(it->first); - if (m_customEffects.Contains(type)) + if (m_customAudioEffects.Contains(type)) continue; - m_customEffects.Add(type, ParseCustomEffect(it->second, m_switchablePaths)); + m_customAudioEffects.Add(type, ParseCustomEffect(it->second, m_switchablePaths)); } for (auto it = kshootMap.filterDefines.begin(); it != kshootMap.filterDefines.end(); it++) { EffectType type = filterTypeMap.FindOrAddEffectType(it->first); - if (m_customFilters.Contains(type)) + if (m_customAudioFilters.Contains(type)) continue; - m_customFilters.Add(type, ParseCustomEffect(it->second, m_switchablePaths)); + m_customAudioFilters.Add(type, ParseCustomEffect(it->second, m_switchablePaths)); } auto ParseFilterType = [&](const String &str) { @@ -586,38 +561,63 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) } } - // Temporary map for timing points - Map timingPointMap; - // Used for accurate time calculations - Map timingPointTicks; - - // Process initial timing point - TimingPoint *lastTimingPoint = new TimingPoint(); - lastTimingPoint->time = atol(*kshootMap.settings["o"]); - double bpm = atof(*kshootMap.settings["t"]); - lastTimingPoint->beatDuration = 60000.0 / bpm; - lastTimingPoint->numerator = 4; - - // Block offset for current timing point - uint32 timingPointBlockOffset = 0; - // Tick offset into block for current timing point - uint32 timingTickOffset = 0; - - // Add First timing point - m_timingPoints.Add(lastTimingPoint); - timingPointMap.Add(lastTimingPoint->time, lastTimingPoint); - timingPointTicks.Add(0, lastTimingPoint); - int tickResolution = 240; + const static int tickResolution = 240; + + const TimingPoint* currTimingPoint = nullptr; + + /// For more accurate tracking of ticks for each timing point + size_t refTimingPointInd = 0; + double refTimingPointTime = 0.0; + + Vector timingPointTicks = {0}; + + auto TickToMapTime = [&](uint32 tick) { + if (tick < timingPointTicks[refTimingPointInd]) + { + refTimingPointInd = 0; + refTimingPointTime = static_cast(m_timingPoints[refTimingPointInd].time); + } + while (refTimingPointInd + 1 < timingPointTicks.size() && timingPointTicks[refTimingPointInd + 1] <= tick) + { + const MapTime timeDiff = timingPointTicks[refTimingPointInd+1] - timingPointTicks[refTimingPointInd]; + refTimingPointTime += Math::MSFromTicks((double) timeDiff, m_timingPoints[refTimingPointInd].GetBPM(), static_cast(tickResolution)); + ++refTimingPointInd; + } + + const uint32 tickDiff = tick - timingPointTicks[refTimingPointInd]; + + double mapTime = refTimingPointTime; + mapTime += Math::MSFromTicks((double) tickDiff, m_timingPoints[refTimingPointInd].GetBPM(), static_cast(tickResolution)); + return Math::RoundToInt(mapTime); + }; + + { + TimingPoint firstTimingPoint; + firstTimingPoint.numerator = 4; + firstTimingPoint.denominator = 4; + + firstTimingPoint.time = atol(*kshootMap.settings["o"]); + refTimingPointTime = static_cast(firstTimingPoint.time); + + const double bpm = atof(*kshootMap.settings["t"]); + firstTimingPoint.beatDuration = 60000.0 / bpm; + + currTimingPoint = &(m_timingPoints.Add(std::move(firstTimingPoint))); + } // Add First Lane Toggle Point - LaneHideTogglePoint *startLaneTogglePoint = new LaneHideTogglePoint(); - startLaneTogglePoint->time = 0; - startLaneTogglePoint->duration = 1; - m_laneTogglePoints.Add(startLaneTogglePoint); + { + LaneHideTogglePoint startLaneTogglePoint; + startLaneTogglePoint.time = 0; + startLaneTogglePoint.duration = 1; + m_laneTogglePoints.Add(std::move(startLaneTogglePoint)); + } // Stop here if we're only going for metadata if (metadataOnly) + { return true; + } // Button hold states TempButtonState *buttonStates[6] = {nullptr}; @@ -631,10 +631,14 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) float laserRanges[2] = {1.0f, 1.0f}; MapTime lastLaserPointTime[2] = {0, 0}; - ZoomControlPoint *firstControlPoints[5] = {nullptr}; + // Stops will be applied after the scroll speed graph is constructed. + // Tuple of (stopBegin, stopEnd, isOverlappingStop) + Vector> stops; + MapTime lastMapTime = 0; uint32 currentTick = 0; - ZoomControlPoint* lastManualTiltPoint = nullptr; + + bool isManualTilt = false; for (KShootMap::TickIterator it(kshootMap); it; ++it) { const KShootBlock &block = it.GetCurrentBlock(); @@ -643,13 +647,12 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) float fxSampleVolume[2] = {1.0, 1.0}; bool useFxSample[2] = {false, false}; uint8 fxSampleIndex[2] = {0, 0}; - MapTime mapTime = MapTimeFromTicks(currentTick, timingPointTicks, tickResolution); + MapTime mapTime = TickToMapTime(currentTick); bool lastTick = &block == &kshootMap.blocks.back() && &tick == &block.ticks.back(); // flag set when a new effect parameter is set and a new hold notes should be created bool splitupHoldNotes[2] = {false, false}; - bool isManualTilt = false; uint32 tickSettingIndex = 0; // Process settings @@ -657,22 +660,21 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) { // Functions that adds a new timing point at current location if it's not yet there auto AddTimingPoint = [&](double newDuration, uint32 newNum, uint32 newDenom, int8 tickrateOffset) { - // Does not yet exist at current time? - if (!timingPointMap.Contains(mapTime)) + if (currTimingPoint->time != mapTime) { - lastTimingPoint = new TimingPoint(*lastTimingPoint); - lastTimingPoint->time = mapTime; - m_timingPoints.Add(lastTimingPoint); - timingPointMap.Add(mapTime, lastTimingPoint); - timingPointTicks.Add(currentTick, lastTimingPoint); - timingPointBlockOffset = time.block; - timingTickOffset = time.tick; + TimingPoint newTimingPoint = TimingPoint(*currTimingPoint); + newTimingPoint.time = mapTime; + + currTimingPoint = &(m_timingPoints.Add(std::move(newTimingPoint))); + timingPointTicks.Add(currentTick); } - lastTimingPoint->numerator = newNum; - lastTimingPoint->denominator = newDenom; - lastTimingPoint->beatDuration = newDuration; - lastTimingPoint->tickrateOffset = tickrateOffset; + TimingPoint& lastTimingPoint = *m_timingPoints.rbegin(); + + lastTimingPoint.numerator = newNum; + lastTimingPoint.denominator = newDenom; + lastTimingPoint.beatDuration = newDuration; + lastTimingPoint.tickrateOffset = tickrateOffset; }; // Parser the effect and parameters of an FX button (1.60) @@ -724,7 +726,7 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) } } else { - m_customEffects.at(*type).SetDefaultEffectParams(paramsOut); + m_customAudioEffects.at(*type).SetDefaultEffectParams(paramsOut); } } @@ -736,21 +738,21 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) String n, d; if (!p.second.Split("/", &n, &d)) assert(false); + uint32 num = atol(*n); uint32 denom = atol(*d); - //assert(denom % 4 == 0); - AddTimingPoint(lastTimingPoint->beatDuration, num, denom, lastTimingPoint->tickrateOffset); + AddTimingPoint(currTimingPoint->beatDuration, num, denom, currTimingPoint->tickrateOffset); } else if (p.first == "t") { - double bpm = atof(*p.second); - AddTimingPoint(60000.0 / bpm, lastTimingPoint->numerator, lastTimingPoint->denominator, lastTimingPoint->tickrateOffset); + const double bpm = atof(*p.second); + AddTimingPoint(60000.0 / bpm, currTimingPoint->numerator, currTimingPoint->denominator, currTimingPoint->tickrateOffset); } else if (p.first == "tickrate_offset") { int8 value = atoi(*p.second); - AddTimingPoint(lastTimingPoint->beatDuration, lastTimingPoint->numerator, lastTimingPoint->denominator, value); + AddTimingPoint(currTimingPoint->beatDuration, currTimingPoint->numerator, currTimingPoint->denominator, value); } else if (p.first == "laserrange_l") { @@ -783,63 +785,48 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) else if (p.first == "filtertype") { // Inser filter type change event - EventObjectState *evt = new EventObjectState(); + EventObjectState* evt = new EventObjectState(); evt->interTickIndex = tickSettingIndex; evt->time = mapTime; evt->key = EventKey::LaserEffectType; evt->data.effectVal = ParseFilterType(p.second); - m_objectStates.Add(*evt); + m_objectStates.emplace_back(std::unique_ptr(*evt)); } else if (p.first == "pfiltergain") { // Inser filter type change event float gain = (float)atol(*p.second) / 100.0f; - EventObjectState *evt = new EventObjectState(); + EventObjectState* evt = new EventObjectState(); evt->interTickIndex = tickSettingIndex; evt->time = mapTime; evt->key = EventKey::LaserEffectMix; evt->data.floatVal = gain; - m_objectStates.Add(*evt); + m_objectStates.emplace_back(std::unique_ptr(*evt)); } else if (p.first == "chokkakuvol") { float vol = (float)atol(*p.second) / 100.0f; - EventObjectState *evt = new EventObjectState(); + EventObjectState* evt = new EventObjectState(); evt->interTickIndex = tickSettingIndex; evt->time = mapTime; evt->key = EventKey::LaserEffectMix; evt->data.floatVal = vol; - m_objectStates.Add(*evt); + m_objectStates.emplace_back(std::unique_ptr(*evt)); } -#define CHECK_FIRST \ - if (!firstControlPoints[point->index]) \ - firstControlPoints[point->index] = point else if (p.first == "zoom_bottom") { - ZoomControlPoint *point = new ZoomControlPoint(); - point->time = mapTime; - point->index = 0; - point->zoom = (float)atol(*p.second) / 100.0f; - m_zoomControlPoints.Add(point); - CHECK_FIRST; + const double value = atol(p.second.data()) / 100.0; + m_effects.InsertGraphValue(EffectTimeline::GraphType::ZOOM_BOTTOM, mapTime, value); } else if (p.first == "zoom_top") { - ZoomControlPoint *point = new ZoomControlPoint(); - point->time = mapTime; - point->index = 1; - point->zoom = (float)(atol(*p.second) / 100.0); - m_zoomControlPoints.Add(point); - CHECK_FIRST; + const double value = atol(p.second.data()) / 100.0; + m_effects.InsertGraphValue(EffectTimeline::GraphType::ZOOM_TOP, mapTime, value); } else if (p.first == "zoom_side") { - ZoomControlPoint *point = new ZoomControlPoint(); - point->time = mapTime; - point->index = 2; - point->zoom = (float)atol(*p.second) / 100.0f; - m_zoomControlPoints.Add(point); - CHECK_FIRST; + const double value = atol(p.second.data()) / 100.0; + m_effects.InsertGraphValue(EffectTimeline::GraphType::SHIFT_X, mapTime, value); } /* OLD USC MANUAL ROLL, KEPT JUST IN CASE else if (p.first == "roll") @@ -854,20 +841,15 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) */ else if (p.first == "lane_toggle") { - LaneHideTogglePoint *point = new LaneHideTogglePoint(); - point->time = mapTime; - point->duration = atol(*p.second); - m_laneTogglePoints.Add(point); + LaneHideTogglePoint point; + point.time = mapTime; + point.duration = atol(*p.second); + m_laneTogglePoints.Add(std::move(point)); } else if (p.first == "center_split") { - ZoomControlPoint *point = new ZoomControlPoint(); - point->time = mapTime; - point->index = 4; - int value = atol(*p.second); - point->zoom = (double)value / 100.0; - m_zoomControlPoints.Add(point); - CHECK_FIRST; + const double value = atol(*p.second) / 100.0; + m_centerSplit.Insert(mapTime, value); } else if (p.first == "tilt") { @@ -897,14 +879,8 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) { evt->data.rollVal = TrackRollBehaviour::Manual; - ZoomControlPoint *point = new ZoomControlPoint(); - point->time = mapTime; - point->index = 3; - point->zoom = atof(*p.second) * -(10.0 / 360.0); - point->instant = lastManualTiltPoint ? lastManualTiltPoint->time == point->time : false; - - lastManualTiltPoint = m_zoomControlPoints.Add(point); - CHECK_FIRST; + const double rotation = atof(p.second.data()) * -(10.0 / 360.0); + m_effects.InsertGraphValue(EffectTimeline::GraphType::ROTATION_Z, mapTime, rotation); isManualTilt = true; goto after_manual_check; @@ -912,16 +888,11 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) if (isManualTilt) { - ZoomControlPoint *point = new ZoomControlPoint(); - point->time = mapTime; - point->index = 3; - point->zoom = m_zoomControlPoints.back()->zoom; - m_zoomControlPoints.Add(point); - CHECK_FIRST; // unnecessary but hey + m_effects.GetGraph(EffectTimeline::GraphType::ROTATION_Z).Extend(mapTime); } after_manual_check: - m_objectStates.Add(*evt); + m_objectStates.emplace_back(std::unique_ptr(*evt)); } else if (p.first == "fx-r_se") { @@ -940,12 +911,12 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) auto it = std::find(m_samplePaths.begin(), m_samplePaths.end(), filename); if (it == m_samplePaths.end()) { - fxSampleIndex[fxi] = m_samplePaths.size(); + fxSampleIndex[fxi] = static_cast(m_samplePaths.size()); m_samplePaths.Add(filename); } else { - fxSampleIndex[fxi] = std::distance(m_samplePaths.begin(), it); + fxSampleIndex[fxi] = static_cast(std::distance(m_samplePaths.begin(), it)); } } else if (p.first == "fx-l_se") @@ -965,7 +936,7 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) auto it = std::find(m_samplePaths.begin(), m_samplePaths.end(), filename); if (it == m_samplePaths.end()) { - fxSampleIndex[fxi] = m_samplePaths.size(); + fxSampleIndex[fxi] = static_cast(m_samplePaths.size()); m_samplePaths.Add(filename); } else @@ -975,10 +946,22 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) } else if (p.first == "stop") { - ChartStop *cs = new ChartStop(); - cs->time = mapTime; - cs->duration = (atol(*p.second) / 192.0f) * (lastTimingPoint->beatDuration) * 4; - m_chartStops.Add(cs); + // Stops will be applied after the scroll speed graph is constructed. + const MapTime stopDuration = Math::RoundToInt((atol(*p.second) / 192.0f) * (currTimingPoint->beatDuration) * 4); + bool isOverlappingStop = false; + + if (!stops.empty() && mapTime < std::get<1>(*stops.rbegin())) + { + isOverlappingStop = true; + std::get<2>(*stops.rbegin()) = true; + } + + stops.Add(std::make_tuple(mapTime, mapTime + stopDuration, isOverlappingStop)); + } + else if (p.first == "scroll_speed") + { + LineGraph& scrollSpeedGraph = m_effects.GetGraph(EffectTimeline::GraphType::SCROLL_SPEED); + scrollSpeedGraph.Insert(mapTime, atol(p.second.data()) / 100.0); } else { @@ -1001,26 +984,26 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) if (IsHoldState()) { HoldObjectState *obj = lastHoldObject = new HoldObjectState(); - obj->time = MapTimeFromTicks(state->startTick, timingPointTicks, tickResolution); + obj->time = TickToMapTime(state->startTick); obj->index = i; - obj->duration = MapTimeFromTicks(currentTick, timingPointTicks, tickResolution) - obj->time; + obj->duration = TickToMapTime(currentTick) - obj->time; obj->effectType = state->effectType; if (state->lastHoldObject) state->lastHoldObject->next = obj; obj->prev = state->lastHoldObject; memcpy(obj->effectParams, state->effectParams, sizeof(state->effectParams)); - m_objectStates.Add(*obj); + m_objectStates.emplace_back(std::unique_ptr(*obj));; } else { ButtonObjectState *obj = new ButtonObjectState(); - obj->time = MapTimeFromTicks(state->startTick, timingPointTicks, tickResolution); + obj->time = TickToMapTime(state->startTick); obj->index = i; obj->hasSample = state->usingSample; obj->sampleIndex = state->sampleIndex; obj->sampleVolume = state->sampleVolume; - m_objectStates.Add(*obj); + m_objectStates.emplace_back(std::unique_ptr(*obj)); } // Reset @@ -1193,9 +1176,9 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) LaserObjectState *obj = new LaserObjectState(); - obj->time = MapTimeFromTicks(state->startTick, timingPointTicks, tickResolution); + obj->time = TickToMapTime(state->startTick); obj->tick = state->startTick; - obj->duration = MapTimeFromTicks(currentTick, timingPointTicks, tickResolution) - obj->time; + obj->duration = TickToMapTime(currentTick) - obj->time; obj->index = i; obj->points[0] = state->startPosition; obj->points[1] = endPos; @@ -1211,7 +1194,7 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) if (tickDuration <= laserSlamThreshold && (obj->points[1] != obj->points[0])) { obj->flags |= LaserObjectState::flag_Instant; - obj->time = MapTimeFromTicks(state->absoluteStartTick, timingPointTicks, tickResolution); + obj->time = TickToMapTime(state->absoluteStartTick); obj->tick = state->absoluteStartTick; if (state->spinType != 0) { @@ -1285,14 +1268,14 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) midobj->next = obj; midobj->prev->next = midobj; - m_objectStates.Add(*midobj); + m_objectStates.emplace_back(std::unique_ptr(*midobj)); } // Add to list of objects assert(obj->GetRoot() != nullptr); - m_objectStates.Add(*obj); + m_objectStates.emplace_back(std::unique_ptr(*obj)); return obj; }; @@ -1337,7 +1320,7 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) // Move offset to be the same as last segment, as in ksh maps there is a 1 tick delay after laser slams startTick = last->tick; } - state = new TempLaserState(startTick, currentTick, 0, lastTimingPoint); + state = new TempLaserState(startTick, currentTick, 0); state->last = last; // Link together state->startPosition = pos; @@ -1378,28 +1361,38 @@ bool Beatmap::m_ProcessKShootMap(BinaryStream &input, bool metadataOnly) } lastMapTime = mapTime; - currentTick += (tickResolution * 4 * lastTimingPoint->numerator / lastTimingPoint->denominator) / block.ticks.size(); + currentTick += static_cast((tickResolution * 4 * currTimingPoint->numerator / currTimingPoint->denominator) / block.ticks.size()); } - for (int i = 0; i < sizeof(firstControlPoints) / sizeof(ZoomControlPoint *); i++) + // Apply stops + for (const auto& stop : stops) { - ZoomControlPoint *point = firstControlPoints[i]; - if (!point) - continue; + const MapTime stopBegin = std::get<0>(stop); + const MapTime stopEnd = std::get<1>(stop); + const bool isOverlapping = std::get<2>(stop); - ZoomControlPoint *dup = new ZoomControlPoint(); - dup->index = point->index; - dup->zoom = point->zoom; - dup->time = INT32_MIN; + LineGraph& scrollSpeedGraph = m_effects.GetGraph(EffectTimeline::GraphType::SCROLL_SPEED); - m_zoomControlPoints.insert(m_zoomControlPoints.begin(), dup); + // In older versions of USC there was a bug where overlapping stop regions made notes scrolling backwards. + // In other words, stops weren't actually setting the scroll speed to 0, but instead decreased the speed by 1. + // This bug was utilized as gimmicks for several charts, so for backwards compatibility this behavior is reimplemented when stops are overlapping. + // For individual stops, scroll speed will actually set to 0 to make those behave nicely with manual scroll speed modifiers. + + if (isOverlapping) + { + scrollSpeedGraph.RangeAdd(stopBegin, stopEnd, -1.0); + } + else + { + scrollSpeedGraph.RangeSet(stopBegin, stopEnd, 0.0); + } } - //Add chart end event + // Add chart end event EventObjectState *evt = new EventObjectState(); evt->time = lastMapTime + 2000; evt->key = EventKey::ChartEnd; - m_objectStates.Add(*evt); + m_objectStates.emplace_back(std::unique_ptr(*evt)); // Re-sort collection to fix some inconsistencies caused by corrections after laser slams ObjectState::SortArray(m_objectStates); diff --git a/Beatmap/src/BeatmapObjects.cpp b/Beatmap/src/BeatmapObjects.cpp index eda7b6567..4fed918af 100644 --- a/Beatmap/src/BeatmapObjects.cpp +++ b/Beatmap/src/BeatmapObjects.cpp @@ -2,24 +2,27 @@ #include "BeatmapObjects.hpp" // Object array sorting -void TObjectState::SortArray(Vector& arr) +void TObjectState::SortArray(std::vector>& arr) { - arr.Sort([](const ObjectState* l, const ObjectState* r) + std::sort(arr.begin(), arr.end(), [](const auto& l, const auto& r) { if(l->time == r->time) { // sort events on the same tick by their index if (l->type == ObjectType::Event && r->type == ObjectType::Event) - return ((EventObjectState*)l)->interTickIndex < ((EventObjectState*)r)->interTickIndex; + return ((EventObjectState*) l.get())->interTickIndex < ((EventObjectState*) r.get())->interTickIndex; // Sort laser slams to come first - bool ls = l->type == ObjectType::Laser && (((LaserObjectState*)l)->flags & LaserObjectState::flag_Instant); - bool rs = r->type == ObjectType::Laser && (((LaserObjectState*)r)->flags & LaserObjectState::flag_Instant); + const bool ls = l->type == ObjectType::Laser && (((LaserObjectState*)l.get())->flags & LaserObjectState::flag_Instant); + const bool rs = r->type == ObjectType::Laser && (((LaserObjectState*)r.get())->flags & LaserObjectState::flag_Instant); + return ls > rs; } + return l->time < r->time; }); } + TObjectState* ObjectTypeData_Hold::GetRoot() { TObjectState* ptr = (TObjectState*)this; @@ -108,7 +111,80 @@ TrackRollBehaviour operator|(const TrackRollBehaviour& l, const TrackRollBehavio { return (TrackRollBehaviour)((uint8)l | (uint8)r); } + TrackRollBehaviour operator&(const TrackRollBehaviour& l, const TrackRollBehaviour& r) { return (TrackRollBehaviour)((uint8)l & (uint8)r); +} + +bool MultiObjectState::StaticSerialize(BinaryStream& stream, MultiObjectState*& obj) +{ + uint8 type = 0; + if (stream.IsReading()) + { + // Read type and create appropriate object + stream << type; + switch ((ObjectType)type) + { + case ObjectType::Single: + obj = (MultiObjectState*)new ButtonObjectState(); + break; + case ObjectType::Hold: + obj = (MultiObjectState*)new HoldObjectState(); + break; + case ObjectType::Laser: + obj = (MultiObjectState*)new LaserObjectState(); + break; + case ObjectType::Event: + obj = (MultiObjectState*)new EventObjectState(); + break; + } + } + else + { + // Write type + type = (uint8)obj->type; + stream << type; + } + + // Pointer is always initialized here, serialize data + stream << obj->time; // Time always set + switch (obj->type) + { + case ObjectType::Single: + stream << obj->button.index; + break; + case ObjectType::Hold: + stream << obj->hold.index; + stream << obj->hold.duration; + stream << (uint16&)obj->hold.effectType; + stream << (int16&)obj->hold.effectParams[0]; + stream << (int16&)obj->hold.effectParams[1]; + break; + case ObjectType::Laser: + stream << obj->laser.index; + stream << obj->laser.duration; + stream << obj->laser.points[0]; + stream << obj->laser.points[1]; + stream << obj->laser.flags; + break; + case ObjectType::Event: + stream << (uint8&)obj->event.key; + stream << *&obj->event.data; + break; + } + + return true; +} + +bool TimingPoint::StaticSerialize(BinaryStream& stream, TimingPoint*& out) +{ + if (stream.IsReading()) + out = new TimingPoint(); + + stream << out->time; + stream << out->beatDuration; + stream << out->numerator; + + return true; } \ No newline at end of file diff --git a/Beatmap/src/BeatmapPlayback.cpp b/Beatmap/src/BeatmapPlayback.cpp index a3f51ee5a..7ab8218bb 100644 --- a/Beatmap/src/BeatmapPlayback.cpp +++ b/Beatmap/src/BeatmapPlayback.cpp @@ -1,69 +1,50 @@ #include "stdafx.h" #include "BeatmapPlayback.hpp" -BeatmapPlayback::BeatmapPlayback(Beatmap& beatmap) : m_beatmap(&beatmap) +BeatmapPlayback::BeatmapPlayback(const Beatmap& beatmap) : m_beatmap(&beatmap) { } -BeatmapPlayback::~BeatmapPlayback() -{ - if (m_isCalibration) { - for (auto* o : m_calibrationObjects) { - delete o; - } - m_calibrationObjects.clear(); - delete m_timingPoints.at(0); - m_timingPoints.clear(); - } -} + bool BeatmapPlayback::Reset(MapTime initTime, MapTime start) { m_effectObjects.clear(); - m_timingPoints = m_beatmap->GetLinearTimingPoints(); - m_chartStops = m_beatmap->GetLinearChartStops(); - m_objects = m_beatmap->GetLinearObjects(); - m_zoomPoints = m_beatmap->GetZoomControlPoints(); - m_laneTogglePoints = m_beatmap->GetLaneTogglePoints(); - if (m_objects.size() == 0) - return false; - if (m_timingPoints.size() == 0) + if (!m_beatmap || !m_beatmap->HasObjectState()) + { return false; + } - Logf("Resetting BeatmapPlayback, InitTime = %d, Start = %d", Logger::Severity::Info, initTime, start); + Logf("Resetting BeatmapPlayback, InitTime = %d, Start = %d", Logger::Severity::Debug, initTime, start); m_playbackTime = initTime; // Ensure that nothing could go wrong when the start is 0 if (start <= 0) start = std::numeric_limits::min(); - m_viewRange = { start, start }; - - m_currentObj = &m_objects.front(); - m_currentAlertObj = &m_objects.front(); - m_currentLaserObj = &m_objects.front(); - m_currentTiming = &m_timingPoints.front(); - m_currentZoomPoint = m_zoomPoints.empty() ? nullptr : &m_zoomPoints.front(); - for (ZoomControlPoint* z : m_zoomPoints) - { - if (z->time != INT32_MIN) //Not a starting point. - break; + m_playRange = { start, start }; - m_zoomStartPoints[z->index] = z; - } - m_currentLaneTogglePoint = m_laneTogglePoints.empty() ? nullptr : &m_laneTogglePoints.front(); + m_currObject = m_beatmap->GetFirstObjectState(); + m_currLaserObject = m_beatmap->GetFirstObjectState(); + m_currAlertObject = m_beatmap->GetFirstObjectState(); - //hittableLaserEnter = (*m_currentTiming)->beatDuration * 4.0; - //alertLaserThreshold = (*m_currentTiming)->beatDuration * 6.0; - m_hittableObjects.clear(); - m_holdObjects.clear(); + m_currentTiming = m_beatmap->GetFirstTimingPoint(); + m_currentLaneTogglePoint = m_beatmap->GetFirstLaneTogglePoint(); + + m_currentTrackRollBehaviour = TrackRollBehaviour::Normal; + m_lastTrackRollBehaviourChange = 0; + + m_objectsByTime.clear(); + m_objectsByLeaveTime.clear(); m_barTime = 0; m_beatTime = 0; m_initialEffectStateSent = false; + return true; } void BeatmapPlayback::Update(MapTime newTime) { MapTime delta = newTime - m_playbackTime; + if (m_isCalibration) { // Count bars int32 beatID = 0; @@ -107,75 +88,86 @@ void BeatmapPlayback::Update(MapTime newTime) m_playbackTime = newTime; // Advance timing - TimingPoint** timingEnd = m_SelectTimingPoint(m_playbackTime); - if (timingEnd != nullptr && timingEnd != m_currentTiming) + Beatmap::TimingPointsIterator timingEnd = m_SelectTimingPoint(m_playbackTime); + if (timingEnd != m_currentTiming) { m_currentTiming = timingEnd; /// TODO: Investigate why this causes score to be too high //hittableLaserEnter = (*m_currentTiming)->beatDuration * 4.0; //alertLaserThreshold = (*m_currentTiming)->beatDuration * 6.0; - OnTimingPointChanged.Call(*m_currentTiming); + OnTimingPointChanged.Call(m_currentTiming); } // Advance lane toggle - LaneHideTogglePoint** laneToggleEnd = m_SelectLaneTogglePoint(m_playbackTime); - if (laneToggleEnd != nullptr && laneToggleEnd != m_currentLaneTogglePoint) + Beatmap::LaneTogglePointsIterator laneToggleEnd = m_SelectLaneTogglePoint(m_playbackTime); + if (laneToggleEnd != m_currentLaneTogglePoint) { m_currentLaneTogglePoint = laneToggleEnd; - OnLaneToggleChanged.Call(*m_currentLaneTogglePoint); + OnLaneToggleChanged.Call(m_currentLaneTogglePoint); } // Advance objects - ObjectState** objEnd = m_SelectHitObject(m_playbackTime + hittableObjectEnter); - if (objEnd != nullptr && objEnd != m_currentObj) + Beatmap::ObjectsIterator objEnd = m_SelectHitObject(m_playbackTime + hittableObjectEnter); + if (objEnd != m_currObject) { - for (auto it = m_currentObj; it < objEnd; it++) + for (auto it = m_currObject; it < objEnd; it++) { - MultiObjectState* obj = **it; + MultiObjectState* obj = *(*it).get(); if (obj->type == ObjectType::Laser) continue; - if (!m_viewRange.Includes(obj->time)) continue; - if (obj->type == ObjectType::Hold && !m_viewRange.Includes(obj->time + obj->hold.duration, true)) continue; + if (!m_playRange.Includes(obj->time)) continue; + if (obj->type == ObjectType::Hold && !m_playRange.Includes(obj->time + obj->hold.duration, true)) continue; - if (obj->type == ObjectType::Hold || obj->type == ObjectType::Single) + MapTime duration = 0; + if (obj->type == ObjectType::Hold) + { + duration = obj->hold.duration; + } + else if (obj->type == ObjectType::Event) { - m_holdObjects.Add(*obj); + // Tiny offset to make sure events are triggered before they are needed + duration = -2; } - m_hittableObjects.AddUnique(*it); - OnObjectEntered.Call(*it); + + m_objectsByTime.Add(obj->time, (*it).get()); + m_objectsByLeaveTime.Add(obj->time + duration + hittableObjectLeave, (*it).get()); + + OnObjectEntered.Call((*it).get()); } - m_currentObj = objEnd; + + m_currObject = objEnd; } // Advance lasers objEnd = m_SelectHitObject(m_playbackTime + hittableLaserEnter); - if (objEnd != nullptr && objEnd != m_currentLaserObj) + if (objEnd != m_currLaserObject) { - for (auto it = m_currentLaserObj; it < objEnd; it++) + for (auto it = m_currLaserObject; it < objEnd; it++) { - MultiObjectState* obj = **it; + MultiObjectState* obj = *(*it).get(); if (obj->type != ObjectType::Laser) continue; - if (!m_viewRange.Includes(obj->time)) continue; - if (!m_viewRange.Includes(obj->time + obj->laser.duration, true)) continue; + if (!m_playRange.Includes(obj->time)) continue; + if (!m_playRange.Includes(obj->time + obj->laser.duration, true)) continue; - m_holdObjects.Add(*obj); - m_hittableObjects.AddUnique(*it); - OnObjectEntered.Call(*it); + m_objectsByTime.Add(obj->time, (*it).get()); + m_objectsByLeaveTime.Add(obj->time + obj->laser.duration + hittableObjectLeave, (*it).get()); + OnObjectEntered.Call((*it).get()); } - m_currentLaserObj = objEnd; + + m_currLaserObject = objEnd; } // Check for lasers within the alert time objEnd = m_SelectHitObject(m_playbackTime + alertLaserThreshold); - if (objEnd != nullptr && objEnd != m_currentAlertObj) + if (objEnd != m_currAlertObject) { - for (auto it = m_currentAlertObj; it < objEnd; it++) + for (auto it = m_currAlertObject; it < objEnd; it++) { MultiObjectState* obj = **it; - if (!m_viewRange.Includes(obj->time)) continue; + if (!m_playRange.Includes(obj->time)) continue; if (obj->type == ObjectType::Laser) { @@ -184,494 +176,397 @@ void BeatmapPlayback::Update(MapTime newTime) OnLaserAlertEntered.Call(laser); } } - m_currentAlertObj = objEnd; + m_currAlertObject = objEnd; } - // Advance zoom points - if (m_currentZoomPoint) + // Check passed objects + for (auto it = m_objectsByLeaveTime.begin(); it != m_objectsByLeaveTime.end() && it->first < m_playbackTime; it = m_objectsByLeaveTime.erase(it)) { - ZoomControlPoint** objEnd = m_SelectZoomObject(m_playbackTime); - for (auto it = m_currentZoomPoint; it < objEnd; it++) + ObjectState* objState = it->second; + MultiObjectState* obj = *(objState); + + // O(n^2) when there are n objects with same time, + // but n is usually small so let's ignore that issue for now... { - // Set this point as new start point - uint32 index = (*it)->index; - m_zoomStartPoints[index] = *it; - - // Set next point - m_zoomEndPoints[index] = nullptr; - ZoomControlPoint** ptr = it + 1; - while (!IsEndZoomPoint(ptr)) + auto pair = m_objectsByTime.equal_range(obj->time); + + for (auto it2 = pair.first; it2 != pair.second; ++it2) { - if ((*ptr)->index == index) + if (it2->second == objState) { - m_zoomEndPoints[index] = *ptr; + m_objectsByTime.erase(it2); break; } - ptr++; } } - m_currentZoomPoint = objEnd; - } - // Check passed hittable objects - MapTime objectPassTime = m_playbackTime - hittableObjectLeave; - for (auto it = m_hittableObjects.begin(); it != m_hittableObjects.end();) - { - MultiObjectState* obj = **it; - if (obj->type == ObjectType::Hold) + switch (obj->type) { - MapTime endTime = obj->hold.duration + obj->time; - if (endTime < objectPassTime) + case ObjectType::Hold: + OnObjectLeaved.Call(objState); + + if (m_effectObjects.Contains(objState)) { - OnObjectLeaved.Call(*it); - it = m_hittableObjects.erase(it); - continue; + OnFXEnd.Call((HoldObjectState*)objState); + m_effectObjects.erase(objState); } - if (obj->hold.effectType != EffectType::None && // Hold button with effect - obj->time - 100 <= m_playbackTime + audioOffset && endTime - 100 > m_playbackTime + audioOffset) // Hold button in active range + break; + case ObjectType::Laser: + case ObjectType::Single: + OnObjectLeaved.Call(objState); + break; + case ObjectType::Event: + { + EventObjectState* evt = (EventObjectState*)obj; + + if (evt->key == EventKey::TrackRollBehaviour) { - if (!m_effectObjects.Contains(*obj)) + if (m_currentTrackRollBehaviour != evt->data.rollVal) { - OnFXBegin.Call((HoldObjectState*)*it); - m_effectObjects.Add(*obj); + m_currentTrackRollBehaviour = evt->data.rollVal; + m_lastTrackRollBehaviourChange = obj->time; } } + + // Trigger event + OnEventChanged.Call(evt->key, evt->data); + m_eventMapping[evt->key] = evt->data; } - else if (obj->type == ObjectType::Laser) - { - if ((obj->laser.duration + obj->time) < objectPassTime) - { - OnObjectLeaved.Call(*it); - it = m_hittableObjects.erase(it); - continue; - } - } - else if (obj->type == ObjectType::Single) - { - if (obj->time < objectPassTime) - { - OnObjectLeaved.Call(*it); - it = m_hittableObjects.erase(it); - continue; - } - } - else if (obj->type == ObjectType::Event) - { - EventObjectState* evt = (EventObjectState*)obj; - if (obj->time < (m_playbackTime + 2)) // Tiny offset to make sure events are triggered before they are needed - { - // Trigger event - OnEventChanged.Call(evt->key, evt->data); - m_eventMapping[evt->key] = evt->data; - it = m_hittableObjects.erase(it); - continue; - } + default: + break; } - it++; } - // Remove passed hold objects - for (auto it = m_holdObjects.begin(); it != m_holdObjects.end();) + const MapTime audioPlaybackTime = m_playbackTime + audioOffset; + + // Process FX effects + for (auto& it : m_objectsByTime) { - MultiObjectState* obj = **it; - if (obj->type == ObjectType::Hold) + ObjectState* objState = it.second; + MultiObjectState* obj = *(objState); + + if (obj->type != ObjectType::Hold || obj->hold.effectType == EffectType::None) { - MapTime endTime = obj->hold.duration + obj->time; - if (endTime < objectPassTime) - { - it = m_holdObjects.erase(it); - continue; - } - if (endTime < m_playbackTime) - { - if (m_effectObjects.Contains(*it)) - { - OnFXEnd.Call((HoldObjectState*)*it); - m_effectObjects.erase(*it); - } - } + continue; } - else if (obj->type == ObjectType::Laser) + + const MapTime endTime = obj->time + obj->hold.duration; + + // Send `OnFXBegin` a little bit earlier (the other side checks the exact timing again) + if (obj->time - 100 <= audioPlaybackTime && audioPlaybackTime <= endTime - 100) { - if ((obj->laser.duration + obj->time) < objectPassTime) + if (!m_effectObjects.Contains(objState)) { - it = m_holdObjects.erase(it); - continue; + OnFXBegin.Call((HoldObjectState*)objState); + m_effectObjects.Add(objState); } } - else if (obj->type == ObjectType::Single) + + if (endTime < audioPlaybackTime) { - if (obj->time < objectPassTime) + if (m_effectObjects.Contains(objState)) { - it = m_holdObjects.erase(it); - continue; + OnFXEnd.Call((HoldObjectState*)objState); + m_effectObjects.erase(objState); } } - it++; } } void BeatmapPlayback::MakeCalibrationPlayback() { m_isCalibration = true; - m_timingPoints.clear(); for (size_t i = 0; i < 50; i++) { ButtonObjectState* newObject = new ButtonObjectState(); newObject->index = i % 4; - newObject->time = i * 500; - m_calibrationObjects.Add((ObjectState*)newObject); + newObject->time = static_cast(i * 500); + + m_calibrationObjects.Add(Ref((ObjectState*)newObject)); } - TimingPoint* calibrationTiming = new TimingPoint(); - calibrationTiming->beatDuration = 500; - calibrationTiming->time = 0; - calibrationTiming->denominator = 4; - calibrationTiming->numerator = 4; - m_timingPoints.Add(calibrationTiming); - m_currentTiming = &m_timingPoints.front(); + m_calibrationTiming = {}; + m_calibrationTiming.beatDuration = 500; + m_calibrationTiming.time = 0; + m_calibrationTiming.denominator = 4; + m_calibrationTiming.numerator = 4; } -ObjectState* BeatmapPlayback::GetFirstButtonOrHoldAfterTime(MapTime time, int lane) +const ObjectState* BeatmapPlayback::GetFirstButtonOrHoldAfterTime(MapTime time, int lane) const { - // Iterate though objects - for (ObjectState** obj = &m_objects.front(); !IsEndObject(obj); obj++) + for (const auto& obj : m_beatmap->GetObjectStates()) { - if (obj[0]->time < time) + if (obj->time < time) continue; - MultiObjectState* mobj = (MultiObjectState*)obj[0]; - if (obj[0]->type != ObjectType::Hold && obj[0]->type != ObjectType::Single) + if (obj->type != ObjectType::Hold && obj->type != ObjectType::Single) continue; + const MultiObjectState* mobj = *(obj.get()); + if (mobj->button.index != lane) continue; - return *obj; + + return obj.get(); } + return nullptr; } -Vector BeatmapPlayback::GetObjectsInRange(MapTime range) +void BeatmapPlayback::GetObjectsInViewRange(float numBeats, Vector& objects) { - static const uint32 earlyVisibility = 200; + // TODO: properly implement backwards scroll speed support... + // numBeats *= 3; - const TimingPoint& tp = GetCurrentTimingPoint(); - - MapTime begin = (MapTime) (m_playbackTime - earlyVisibility); - MapTime end = m_playbackTime + range; - - Vector ret; + const static MapTime earlyVisibility = 200; if (m_isCalibration) { for (auto& o : m_calibrationObjects) { - if (o->time < begin) + if (o->time < (MapTime)(m_playbackTime - earlyVisibility)) continue; - if (o->time > end) + + if (o->time > m_playbackTime + static_cast(numBeats * m_calibrationTiming.beatDuration)) break; - ret.Add(o); + objects.Add(o.get()); } - return ret; - } - if (begin < m_viewRange.begin) begin = m_viewRange.begin; - if (m_viewRange.HasEnd() && end >= m_viewRange.end) end = m_viewRange.end; + return; + } - // Add hold objects - for (auto& ho : m_holdObjects) + // Add objects + for (auto& it : m_objectsByTime) { - ret.AddUnique(ho); + objects.Add(it.second); } - // Iterator - ObjectState** obj = m_currentObj; - // Return all objects that lie after the currently queued object and fall within the given range - while (!IsEndObject(obj)) + Beatmap::TimingPointsIterator tp = m_SelectTimingPoint(m_playbackTime); + Beatmap::TimingPointsIterator tp_next = std::next(tp); + + // # of beats from m_playbackTime to curr TP + MapTime currRefTime = m_playbackTime; + float currBeats = 0.0f; + + for (Beatmap::ObjectsIterator obj = m_currObject; !IsEndObject(obj) && m_playRange.Includes((*obj)->time); ++obj) { - if ((*obj)->time < begin) + const MapTime objTime = (*obj)->time; + + if (!IsEndTiming(tp_next) && tp_next->time <= objTime) { - obj += 1; - continue; + currBeats += m_beatmap->GetBeatCountWithScrollSpeedApplied(currRefTime, tp_next->time, tp); + + tp = tp_next; + tp_next = std::next(tp_next); + currRefTime = tp->time; + } + + const float objBeats = currBeats + m_beatmap->GetBeatCountWithScrollSpeedApplied(currRefTime, objTime, tp); + if (objBeats >= numBeats) + { + break; } - if ((*obj)->time >= end) - break; // No more objects + // Lasers might be already added before + if ((*obj)->type == ObjectType::Laser && obj < m_currLaserObject) + { + continue; + } - ret.AddUnique(*obj); - obj += 1; // Next + objects.Add(obj->get()); } - - return ret; } -const TimingPoint& BeatmapPlayback::GetCurrentTimingPoint() const +void BeatmapPlayback::GetBarPositionsInViewRange(float numBeats, Vector& barPositions) const { - if (!m_currentTiming) - return *m_timingPoints.front(); - return **m_currentTiming; -} -const TimingPoint* BeatmapPlayback::GetTimingPointAt(MapTime time) const -{ - return *const_cast(this)->m_SelectTimingPoint(time); -} + Beatmap::TimingPointsIterator tp = m_SelectTimingPoint(m_playbackTime); + assert(!IsEndTiming(tp)); -uint32 BeatmapPlayback::CountBeats(MapTime start, MapTime range, int32& startIndex, uint32 multiplier /*= 1*/) const -{ - const TimingPoint& tp = GetCurrentTimingPoint(); - int64 delta = (int64)start - (int64)tp.time; - double beatDuration = tp.GetWholeNoteLength() / tp.denominator; - int64 beatStart = (int64)floor((double)delta / (beatDuration / multiplier)); - int64 beatEnd = (int64)floor((double)(delta + range) / (beatDuration / multiplier)); - startIndex = ((int32)beatStart + 1) % tp.numerator; - return (uint32)Math::Max(beatEnd - beatStart, 0); -} -MapTime BeatmapPlayback::ViewDistanceToDuration(float distance) -{ - TimingPoint** tp = m_SelectTimingPoint(m_playbackTime, true); + uint64 measureNo = 0; + + { + MapTime offset = m_playbackTime - tp->time; + if (offset < 0) offset = 0; - double time = 0; + measureNo = static_cast(static_cast(offset) / tp->GetBarDuration()); + } + + MapTime currTime = tp->time + static_cast(measureNo * tp->GetBarDuration()); - MapTime currentTime = m_playbackTime; while (true) { - if (!IsEndTiming(tp + 1)) + barPositions.Add(TimeToViewDistance(currTime)); + + Beatmap::TimingPointsIterator ntp = next(tp); + currTime = tp->time + static_cast(++measureNo * tp->GetBarDuration()); + + if (!IsEndTiming(ntp) && currTime >= ntp->time) { - double maxDist = (tp[1]->time - (double)currentTime) / tp[0]->beatDuration; - if (maxDist < distance) - { - // Split up - time += maxDist * tp[0]->beatDuration; - distance -= (float)maxDist; - tp++; - continue; - } + tp = ntp; + currTime = ntp->time; + measureNo = 0; } - time += distance * tp[0]->beatDuration; - break; - } - /// TODO: Optimize? - uint32 processedStops = 0; - Vector ignoreStops; - do - { - processedStops = 0; - for (auto cs : m_SelectChartStops(currentTime, time)) + // Arbitrary cutoff + if (measureNo >= 1000) { - if (std::find(ignoreStops.begin(), ignoreStops.end(), cs) != ignoreStops.end()) - continue; - time += cs->duration; - processedStops++; - ignoreStops.Add(cs); + return; } - } while (processedStops); - return (MapTime)time; -} -float BeatmapPlayback::DurationToViewDistance(MapTime duration) -{ - return DurationToViewDistanceAtTime(m_playbackTime, duration); + if (m_beatmap->GetBeatCountWithScrollSpeedApplied(m_playbackTime, currTime, tp) >= numBeats) + { + return; + } + } } -float BeatmapPlayback::DurationToViewDistanceAtTimeNoStops(MapTime time, MapTime duration) +const TimingPoint& BeatmapPlayback::GetCurrentTimingPoint() const { - MapTime endTime = time + duration; - int8 direction = Math::Sign(duration); - if (duration < 0) + if (m_isCalibration) { - MapTime temp = time; - time = endTime; - endTime = temp; - duration *= -1; + return m_calibrationTiming; } - // Accumulated value - double barTime = 0.0f; - - // Split up to see if passing other timing points on the way - TimingPoint** tp = m_SelectTimingPoint(time, true); - while (true) + if (IsEndTiming(m_currentTiming)) { - if (!IsEndTiming(tp + 1)) - { - if (tp[1]->time < endTime) - { - // Split up - MapTime myDuration = tp[1]->time - time; - barTime += (double)myDuration / tp[0]->beatDuration; - duration -= myDuration; - time = tp[1]->time; - tp++; - continue; - } - } - // Whole - barTime += (double)duration / tp[0]->beatDuration; - break; + return *(m_beatmap->GetFirstTimingPoint()); } - return (float)barTime * direction; + return *m_currentTiming; } -float BeatmapPlayback::DurationToViewDistanceAtTime(MapTime time, MapTime duration) +const TimingPoint* BeatmapPlayback::GetTimingPointAt(MapTime time) const { - if (cMod) + if (m_isCalibration) { - return (float)duration / 480000.0f; + return &m_calibrationTiming; } - MapTime endTime = time + duration; - int8 direction = Math::Sign(duration); - if (duration < 0) + + Beatmap::TimingPointsIterator it = const_cast(this)->m_SelectTimingPoint(time); + if (IsEndTiming(it)) { - MapTime temp = time; - time = endTime; - endTime = temp; - duration *= -1; + return nullptr; } + else + { + return &(*it); + } +} - MapTime startTime = time; - - // Accumulated value - double barTime = 0.0f; +uint32 BeatmapPlayback::CountBeats(MapTime start, MapTime range, int32& startIndex, uint32 multiplier /*= 1*/) const +{ + const TimingPoint& tp = GetCurrentTimingPoint(); + int64 delta = (int64)start - (int64)tp.time; + double beatDuration = tp.GetWholeNoteLength() / tp.denominator; + int64 beatStart = (int64)floor((double)delta / (beatDuration / multiplier)); + int64 beatEnd = (int64)floor((double)(delta + range) / (beatDuration / multiplier)); + startIndex = ((int32)beatStart + 1) % tp.numerator; + return (uint32)Math::Max(beatEnd - beatStart, 0); +} - // Split up to see if passing other timing points on the way - TimingPoint** tp = m_SelectTimingPoint(time, true); - while (true) +float BeatmapPlayback::GetViewDistance(MapTime startTime, MapTime endTime) const +{ + if (startTime == endTime) { - if (!IsEndTiming(tp + 1)) - { - if (tp[1]->time < endTime) - { - // Split up - MapTime myDuration = tp[1]->time - time; - barTime += (double)myDuration / tp[0]->beatDuration; - duration -= myDuration; - time = tp[1]->time; - tp++; - continue; - } - } - // Whole - barTime += (double)duration / tp[0]->beatDuration; - break; + return 0.0f; } - // calculate stop ViewDistance - double stopTime = 0.; - for (auto cs : m_SelectChartStops(startTime, endTime - startTime)) + if (cMod || m_isCalibration) { - MapTime overlap = Math::Min(abs(endTime - startTime), - Math::Min(abs(endTime - cs->time), - Math::Min(abs((cs->time + cs->duration) - startTime), abs((cs->time + cs->duration) - cs->time)))); - - stopTime += DurationToViewDistanceAtTimeNoStops(Math::Max(cs->time, startTime), overlap); + return GetViewDistanceIgnoringScrollSpeed(startTime, endTime); } - barTime -= stopTime; - - return (float)barTime * direction; + return m_beatmap->GetBeatCountWithScrollSpeedApplied(startTime, endTime, m_currentTiming); } -float BeatmapPlayback::TimeToViewDistance(MapTime time) +float BeatmapPlayback::GetViewDistanceIgnoringScrollSpeed(MapTime startTime, MapTime endTime) const { + if (startTime == endTime) + { + return 0.0f; + } + if (cMod) - return (float)(time - m_playbackTime) / (480000.f); + { + return static_cast(endTime - startTime) / 480000.0f; + } - return DurationToViewDistanceAtTime(m_playbackTime, time - m_playbackTime); -} + if (m_isCalibration) + { + return static_cast((endTime - startTime) / m_calibrationTiming.beatDuration); + } -float BeatmapPlayback::GetBarTime() const -{ - return m_barTime; + return m_beatmap->GetBeatCount(startTime, endTime, m_currentTiming); } -float BeatmapPlayback::GetBeatTime() const +float BeatmapPlayback::GetZoom(uint8 index) const { - return m_beatTime; -} + EffectTimeline::GraphType graphType; -float BeatmapPlayback::GetZoom(uint8 index) -{ - assert(index >= 0 && index <= 4); - MapTime startTime = m_zoomStartPoints[index] ? m_zoomStartPoints[index]->time : 0; - float start = m_zoomStartPoints[index] ? m_zoomStartPoints[index]->zoom : 0.0f; - if (!m_zoomEndPoints[index]) // Last point? - return start; - - // Interpolate - MapTime duration = m_zoomEndPoints[index]->time - startTime; - MapTime currentOffsetInto = m_playbackTime - startTime; - float zoomDelta = m_zoomEndPoints[index]->zoom - start; - float f = (float)currentOffsetInto / (float)duration; - return start + zoomDelta * f; -} + switch (index) + { + case 0: + graphType = EffectTimeline::GraphType::ZOOM_BOTTOM; + break; + case 1: + graphType = EffectTimeline::GraphType::ZOOM_TOP; + break; + case 2: + graphType = EffectTimeline::GraphType::SHIFT_X; + break; + case 3: + graphType = EffectTimeline::GraphType::ROTATION_Z; + break; + case 4: + return m_beatmap->GetCenterSplitValueAt(m_playbackTime); + break; + default: + assert(false); + break; + } -bool BeatmapPlayback::CheckIfManualTiltInstant() -{ - return m_zoomStartPoints[3] ? m_zoomStartPoints[3]->instant : false; + return m_beatmap->GetGraphValueAt(graphType, m_playbackTime); } -MapTime BeatmapPlayback::GetLastTime() const +float BeatmapPlayback::GetScrollSpeed() const { - return m_playbackTime; + return m_beatmap->GetScrollSpeedAt(m_playbackTime); } -TimingPoint** BeatmapPlayback::m_SelectTimingPoint(MapTime time, bool allowReset) -{ - TimingPoint** objStart = m_currentTiming; - if (IsEndTiming(objStart)) - return objStart; - - // Start at front of array if current object lies ahead of given input time - if (objStart[0]->time > time && allowReset) - objStart = &m_timingPoints.front(); - // Keep advancing the start pointer while the next object's starting time lies before the input time - while (true) +bool BeatmapPlayback::CheckIfManualTiltInstant() +{ + if (m_currentTrackRollBehaviour != TrackRollBehaviour::Manual) { - if (!IsEndTiming(objStart + 1) && objStart[1]->time <= time) - { - objStart = objStart + 1; - } - else - break; + return false; } - return objStart; + return m_beatmap->CheckIfManualTiltInstant(m_lastTrackRollBehaviourChange, m_playbackTime); } -Vector BeatmapPlayback::m_SelectChartStops(MapTime time, MapTime duration) +Beatmap::TimingPointsIterator BeatmapPlayback::m_SelectTimingPoint(MapTime time, bool allowReset) const { - Vector stops; - for (auto cs : m_chartStops) - { - if (time <= cs->time + cs->duration && time + duration >= cs->time) - stops.Add(cs); - } - return stops; + return m_beatmap->GetTimingPoint(time, m_currentTiming, !allowReset); } - - -LaneHideTogglePoint** BeatmapPlayback::m_SelectLaneTogglePoint(MapTime time, bool allowReset) +Beatmap::LaneTogglePointsIterator BeatmapPlayback::m_SelectLaneTogglePoint(MapTime time, bool allowReset) const { - LaneHideTogglePoint** objStart = m_currentLaneTogglePoint; + Beatmap::LaneTogglePointsIterator objStart = m_currentLaneTogglePoint; if (IsEndLaneToggle(objStart)) return objStart; // Start at front of array if current object lies ahead of given input time - if (objStart[0]->time > time && allowReset) - objStart = &m_laneTogglePoints.front(); + if (objStart->time > time && allowReset) + objStart = m_beatmap->GetFirstLaneTogglePoint(); // Keep advancing the start pointer while the next object's starting time lies before the input time while (true) { - if (!IsEndLaneToggle(objStart + 1) && objStart[1]->time <= time) + if (!IsEndLaneToggle(objStart + 1) && (objStart + 1)->time <= time) { objStart = objStart + 1; - } + } else break; } @@ -679,43 +574,23 @@ LaneHideTogglePoint** BeatmapPlayback::m_SelectLaneTogglePoint(MapTime time, boo return objStart; } - -ObjectState** BeatmapPlayback::m_SelectHitObject(MapTime time, bool allowReset) +Beatmap::ObjectsIterator BeatmapPlayback::m_SelectHitObject(MapTime time, bool allowReset) const { - ObjectState** objStart = m_currentObj; + Beatmap::ObjectsIterator objStart = m_currObject; if (IsEndObject(objStart)) return objStart; // Start at front of array if current object lies ahead of given input time if (objStart[0]->time > time && allowReset) - objStart = &m_objects.front(); + objStart = m_beatmap->GetFirstObjectState(); // Keep advancing the start pointer while the next object's starting time lies before the input time while (true) { if (!IsEndObject(objStart) && objStart[0]->time < time) { - objStart = objStart + 1; - } - else - break; - } - - return objStart; -} -ZoomControlPoint** BeatmapPlayback::m_SelectZoomObject(MapTime time) -{ - ZoomControlPoint** objStart = m_currentZoomPoint; - if (IsEndZoomPoint(objStart)) - return objStart; - - // Keep advancing the start pointer while the next object's starting time lies before the input time - while (true) - { - if (!IsEndZoomPoint(objStart) && objStart[0]->time < time) - { - objStart = objStart + 1; - } + objStart = std::next(objStart); + } else break; } @@ -723,21 +598,17 @@ ZoomControlPoint** BeatmapPlayback::m_SelectZoomObject(MapTime time) return objStart; } -bool BeatmapPlayback::IsEndTiming(TimingPoint** obj) +bool BeatmapPlayback::IsEndObject(const Beatmap::ObjectsIterator& obj) const { - return obj == (&m_timingPoints.back() + 1); -} -bool BeatmapPlayback::IsEndObject(ObjectState** obj) -{ - return obj == (&m_objects.back() + 1); + return obj == m_beatmap->GetEndObjectState(); } -bool BeatmapPlayback::IsEndLaneToggle(LaneHideTogglePoint** obj) +bool BeatmapPlayback::IsEndTiming(const Beatmap::TimingPointsIterator& obj) const { - return obj == (&m_laneTogglePoints.back() + 1); + return obj == m_beatmap->GetEndTimingPoint(); } -bool BeatmapPlayback::IsEndZoomPoint(ZoomControlPoint** obj) +bool BeatmapPlayback::IsEndLaneToggle(const Beatmap::LaneTogglePointsIterator& obj) const { - return obj == (&m_zoomPoints.back() + 1); -} + return obj == m_beatmap->GetEndLaneTogglePoint(); +} \ No newline at end of file diff --git a/Beatmap/src/ChallengeIndex.cpp b/Beatmap/src/ChallengeIndex.cpp index ec1974ec7..ae9a1daff 100644 --- a/Beatmap/src/ChallengeIndex.cpp +++ b/Beatmap/src/ChallengeIndex.cpp @@ -232,7 +232,8 @@ void ChallengeIndex::GenerateDescription() desc += "\n"; const auto& o = settings["overrides"]; - unsigned int maxNum = std::min(std::min((size_t)totalNumCharts, charts.size()), o.size()); + unsigned int maxNum = static_cast(std::min(std::min((size_t)totalNumCharts, charts.size()), o.size())); + for (unsigned int i = 0; i < maxNum; i++) { String overdesc = ""; @@ -298,7 +299,7 @@ void ChallengeIndex::FindCharts(MapDatabase* db, const nlohmann::json& chartsToF return; } - totalNumCharts = chartsToFind.size(); + totalNumCharts = static_cast(chartsToFind.size()); for (auto& el : chartsToFind.items()) { ChartIndex* chart = nullptr; diff --git a/Beatmap/src/EffectTimeline.cpp b/Beatmap/src/EffectTimeline.cpp new file mode 100644 index 000000000..a3f98ffc8 --- /dev/null +++ b/Beatmap/src/EffectTimeline.cpp @@ -0,0 +1,2 @@ +#include "stdafx.h" +#include "EffectTimeline.hpp" \ No newline at end of file diff --git a/Beatmap/src/LineGraph.cpp b/Beatmap/src/LineGraph.cpp new file mode 100644 index 000000000..be6f4baa6 --- /dev/null +++ b/Beatmap/src/LineGraph.cpp @@ -0,0 +1,307 @@ +/// From https://github.com/m4saka/ksh + +#include "stdafx.h" +#include "LineGraph.hpp" + +void LineGraph::Insert(MapTime mapTime, double point) +{ + auto it = m_points.find(mapTime); + if (it == m_points.end()) + { + m_points.emplace_hint(it, mapTime, Point{point}); + } + else + { + it->second.value.second = point; + } +} + +void LineGraph::Insert(MapTime mapTime, const LineGraph::Point& point) +{ + auto it = m_points.find(mapTime); + if (it == m_points.end()) + { + m_points.emplace_hint(it, mapTime, point); + } + else + { + it->second.value.second = point.value.second; + } +} + +void LineGraph::Insert(MapTime mapTime, const std::string& point) +{ + const std::size_t semicolonIdx = point.find(';'); + if (semicolonIdx == std::string::npos) + { + try + { + Insert(mapTime, std::stod(point)); + } + catch (const std::invalid_argument&) {} + catch (const std::out_of_range&) {} + } + else + { + Insert(mapTime, LineGraph::Point{std::stod(point.substr(semicolonIdx + 1)), std::stod(point.substr(semicolonIdx + 1))}); + } +} + +void LineGraph::RangeSet(MapTime begin, MapTime end, double value) +{ + if (begin >= end) return; + + const double beginValue = ValueAt(begin); + const double endValue = ValueAt(end); + + const auto beginIt = m_points.lower_bound(begin); + const auto endIt = m_points.upper_bound(end); + + for (auto it = beginIt; it != endIt; it = m_points.erase(it)); + + Insert(begin, LineGraph::Point{beginValue, value}); + Insert(end, LineGraph::Point{value, endValue}); +} + +void LineGraph::RangeAdd(MapTime begin, MapTime end, double delta) +{ + if (begin >= end) return; + + const double beginValue = ValueAt(begin); + const double endValue = ValueAt(end); + + const auto beginIt = m_points.upper_bound(begin); + const auto endIt = m_points.lower_bound(end); + + for (auto it = beginIt; it != endIt; ++it) + { + it->second.value.first += delta; + it->second.value.second += delta; + } + + Insert(begin, LineGraph::Point{beginValue, beginValue + delta}); + + if (endIt != m_points.end() && endIt->first == end) + { + endIt->second.value.first += delta; + } else + { + Insert(end, LineGraph::Point{endValue + delta, endValue}); + } +} + +double LineGraph::Extend(MapTime time) +{ + if (m_points.empty()) + { + Insert(time, m_default); + return m_default; + } + + auto it = m_points.upper_bound(time); + + if (it == m_points.begin()) + { + return it->second.value.first; + } + + it = std::prev(it); + + if (it->first == time) + { + return it->second.value.first; + } + + const double value = it->second.value.second; + Insert(time, value); + + return value; +} + +double LineGraph::Integrate(MapTime begin, MapTime end) const +{ + int sign = 1; + + if (begin == end) + { + return 0.0; + } + + if (m_points.size() == 0) + { + return (end - begin) * m_default; + } + + if (end < begin) + { + std::swap(begin, end); + sign = -1; + } + + // Integration range is after the last point + auto beginIt = m_points.upper_bound(begin); + if (beginIt == m_points.end()) + { + return sign * m_points.rbegin()->second.value.second * (end - begin); + } + + if (end <= beginIt->first) + { + if (beginIt == m_points.begin()) + { + // Integration range is before the first point + return sign * beginIt->second.value.first * (end - begin); + } + else + { + // Integration range contained in a single segment + return sign * Integrate(std::prev(beginIt), begin, end); + } + } + + double result = 0.0; + + // Ensure that the beginning of the integration range is beginIt + if (beginIt == m_points.begin()) + { + result = beginIt->second.value.first * (beginIt->first - begin); + } + else if (begin != beginIt->first) + { + auto beginPrev = std::prev(beginIt); + result = Integrate(beginPrev, begin, beginIt->first); + } + + auto endIt = m_points.upper_bound(end); + if (endIt == m_points.begin()) + { + // This means that end < m_points.begin()->first + // But then, since begin < end, begin < m_points.begin()->first so beginIt == m_points.begin() + // Therefore end < beginIt->first and this case is already handled + // But let's check for this case just to be sure + assert(false); + return sign * endIt->second.value.first * (end - begin); + } + + endIt = std::prev(endIt); + + // Ensure that the end of the integration range is endIt + if (endIt->first != end) + { + result += Integrate(endIt, endIt->first, end); + } + + // Integrate the remaining part + for (; beginIt != endIt; ++beginIt) + { + result += Integrate(beginIt); + } + + return sign * result; +} + +double LineGraph::Integrate(PointsIterator curr, MapTime begin, MapTime end) const +{ + int sign = 1; + + if (begin == end) + { + return 0.0; + } + + if (end < begin) + { + std::swap(begin, end); + sign = -1; + } + + if (m_points.empty()) + { + return sign * (end - begin) * m_default; + } + + if (curr == m_points.end()) + { + return sign * m_points.rbegin()->second.value.second * (end - begin); + } + + assert(curr->first <= begin); + + auto next = std::next(curr); + if (next == m_points.end()) + { + return sign * curr->second.value.second * (end - begin); + } + + assert(end <= next->first); + + // TODO: support integration of bezier curves + double value = Integrate(curr); + + if (curr->first != begin) + { + const double x = static_cast(begin - curr->first) / (next->first - curr->first); + value -= (begin - curr->first) * Math::Lerp(curr->second.value.second, next->second.value.first, x * 0.5); + } + + if (end != next->first) + { + const double x = static_cast(next->first - end) / (next->first - curr->first); + value -= (next->first - end) * Math::Lerp(curr->second.value.second, next->second.value.first, 1 - x * 0.5); + } + + return sign * value; +} + +double LineGraph::Integrate(PointsIterator curr) const +{ + if (m_points.empty() || curr == m_points.end()) + { + return 0.0; + } + + auto next = std::next(curr); + if (next == m_points.end()) + { + assert(false); + return 0.0; + } + + return static_cast(next->first - curr->first) * (next->second.value.first + curr->second.value.second) * 0.5; +} + +double LineGraph::ValueAt(MapTime mapTime) const +{ + if (m_points.empty()) + { + return m_default; + } + + const auto secondItr = m_points.upper_bound(mapTime); + if (secondItr == m_points.begin()) + { + // Before the first plot + return secondItr->second.value.first; + } + + const auto firstItr = std::prev(secondItr); + const double firstValue = (*firstItr).second.value.second; + + if (secondItr == m_points.end()) + { + // After the last plot + return firstValue; + } + + const double secondValue = secondItr->second.value.first; + const MapTime firstTime = firstItr->first; + const MapTime secondTime = secondItr->first; + + // Erratic case + if (firstTime == secondTime) + { + return secondValue; + } + + return Math::Lerp(firstValue, secondValue, (mapTime - firstTime) / static_cast(secondTime - firstTime)); +} \ No newline at end of file diff --git a/Beatmap/stdafx.h b/Beatmap/stdafx.h index 267aa59f6..1df97f24a 100644 --- a/Beatmap/stdafx.h +++ b/Beatmap/stdafx.h @@ -2,4 +2,9 @@ #pragma once #include -#include \ No newline at end of file + +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/Main/src/Application.cpp b/Main/src/Application.cpp index 20171365b..0a2e453d8 100644 --- a/Main/src/Application.cpp +++ b/Main/src/Application.cpp @@ -882,6 +882,7 @@ bool Application::m_Init() { if (cl == "-convertmaps") { + // Note: this feature should be re-implemented. See `Beatmap.cpp`. m_allowMapConversion = true; } else if (cl == "-mute") diff --git a/Main/src/Audio/AudioPlayback.cpp b/Main/src/Audio/AudioPlayback.cpp index 39738a57f..ab41ec96c 100644 --- a/Main/src/Audio/AudioPlayback.cpp +++ b/Main/src/Audio/AudioPlayback.cpp @@ -521,11 +521,11 @@ void AudioPlayback::m_PreRenderDSPTrack() { ProfilerScope $("Pre-rendering FX effects"); Vector DSPs; - for (auto chartObj : m_playback->GetBeatmap().GetLinearObjects()) + for (const auto& chartObj : m_playback->GetBeatmap().GetObjectStates()) { if (chartObj->type == ObjectType::Hold) { - HoldObjectState *holdObj = (HoldObjectState *)chartObj; + HoldObjectState *holdObj = (HoldObjectState *) chartObj.get(); if (holdObj->effectType != EffectType::None) { //Add DSP diff --git a/Main/src/Audio/OffsetComputer.cpp b/Main/src/Audio/OffsetComputer.cpp index 7f0692276..0318abf00 100644 --- a/Main/src/Audio/OffsetComputer.cpp +++ b/Main/src/Audio/OffsetComputer.cpp @@ -70,7 +70,7 @@ bool OffsetComputer::Compute(int& outOffset) return false; } - Logf("OffsetComputer::Compute: Using %d beats starting from %d...", Logger::Severity::Info, + Logf("OffsetComputer::Compute: Using %d beats starting from %d...", Logger::Severity::Debug, m_beats.size(), m_beats[0].time); m_offsetCenter = outOffset; @@ -141,10 +141,10 @@ void OffsetComputer::ReadBeats() int maxBeatsBeginInd = 0; int maxBeatsCount = 0; - const Vector& timingPoints = m_beatmap.GetLinearTimingPoints(); + const Vector& timingPoints = m_beatmap.GetTimingPoints(); int timingPointInd = 0; - for (const ObjectState* object : m_beatmap.GetLinearObjects()) + for (const auto& object : m_beatmap.GetObjectStates()) { MapTime currBeat = lastBeat; switch (object->type) @@ -171,15 +171,15 @@ void OffsetComputer::ReadBeats() { if (timingPointInd + 1 < static_cast(timingPoints.size())) { - if (timingPoints[timingPointInd + 1]->time <= currBeat) + if (timingPoints[timingPointInd + 1].time <= currBeat) ++timingPointInd; } - const TimingPoint* timingPoint = timingPoints[timingPointInd]; - const double barDuration = timingPoint->GetBarDuration(); - const double beatDuration = barDuration / timingPoint->numerator; + const TimingPoint& timingPoint = timingPoints[timingPointInd]; + const double barDuration = timingPoint.GetBarDuration(); + const double beatDuration = barDuration / timingPoint.numerator; - double timingOffset = static_cast(currBeat - timingPoint->time); + double timingOffset = static_cast(currBeat - timingPoint.time); weight = static_cast(GetBeatWeight(timingOffset / barDuration) * 0.75 + GetBeatWeight(timingOffset / beatDuration) * 0.25); } diff --git a/Main/src/CalibrationScreen.cpp b/Main/src/CalibrationScreen.cpp index 48f29e0e1..8ffd10e75 100644 --- a/Main/src/CalibrationScreen.cpp +++ b/Main/src/CalibrationScreen.cpp @@ -72,8 +72,8 @@ void CalibrationScreen::Render(float deltaTime) RenderState rs = m_camera.CreateRenderState(true); RenderQueue renderQueue(g_gl, rs); - MapTime msViewRange = m_playback.ViewDistanceToDuration(m_track.GetViewRange()); - auto currentObjectSet = m_playback.GetObjectsInRange(msViewRange); + Vector currentObjectSet; + m_playback.GetObjectsInViewRange(m_track.GetViewRange(), currentObjectSet); m_track.DrawBase(renderQueue); std::unordered_set chipFXTimes[2]; diff --git a/Main/src/Game.cpp b/Main/src/Game.cpp index a48233cc5..2862b302c 100755 --- a/Main/src/Game.cpp +++ b/Main/src/Game.cpp @@ -177,8 +177,8 @@ class Game_Impl : public Game Vector m_scoreReplays; MapDatabase* m_db; - std::unordered_set m_hiddenObjects; - std::unordered_set m_permanentlyHiddenObjects; + std::unordered_set m_hiddenObjects; + std::unordered_set m_permanentlyHiddenObjects; // Hold detection for restart and exit MapTime m_restartTriggerTime = 0; @@ -326,61 +326,22 @@ class Game_Impl : public Game const BeatmapSettings& mapSettings = m_beatmap->GetMapSettings(); - // Move this somewhere else? // Set hi-speed for m-Mod - // Uses the "mode" of BPMs in the chart, should use median? if(m_speedMod == SpeedMods::MMod) { - Map bpmDurations; - const Vector& timingPoints = m_beatmap->GetLinearTimingPoints(); - MapTime lastMT = mapSettings.offset; - MapTime largestMT = -1; - double useBPM = -1; - double lastBPM = -1; - - if (mapSettings.speedBpm > 0.0f) - { - useBPM = mapSettings.speedBpm; - } - else - { - for (TimingPoint* tp : timingPoints) - { - double thisBPM = tp->GetBPM(); - if (!bpmDurations.count(lastBPM)) - { - bpmDurations[lastBPM] = 0; - } - MapTime timeSinceLastTP = tp->time - lastMT; - bpmDurations[lastBPM] += timeSinceLastTP; - if (bpmDurations[lastBPM] > largestMT) - { - useBPM = lastBPM; - largestMT = bpmDurations[lastBPM]; - } - lastMT = tp->time; - lastBPM = thisBPM; - } - bpmDurations[lastBPM] += m_endTime - lastMT; - - if (bpmDurations[lastBPM] > largestMT) - { - useBPM = lastBPM; - } - } - - m_hispeed = m_modSpeed / useBPM; - CheckChallengeHispeed(useBPM); + const double modeBPM = m_beatmap->GetModeBPM(); + m_hispeed = m_modSpeed / modeBPM; + CheckChallengeHispeed(modeBPM); } else if (m_speedMod == SpeedMods::CMod) { - double bpm = m_beatmap->GetLinearTimingPoints().front()->GetBPM(); + const double bpm = m_beatmap->GetFirstTimingPoint()->GetBPM(); m_hispeed = m_modSpeed / bpm; CheckChallengeHispeed(bpm); } else if (m_speedMod == SpeedMods::XMod) { - CheckChallengeHispeed(m_beatmap->GetLinearTimingPoints().front()->GetBPM()); + CheckChallengeHispeed(m_beatmap->GetFirstTimingPoint()->GetBPM()); } // Load replays @@ -571,76 +532,9 @@ class Game_Impl : public Game m_track->hitEffectAutoplay = m_scoring.autoplayInfo.IsAutoplayButtons(); - if (GetPlaybackOptions().random) + if (GetPlaybackOptions().random || GetPlaybackOptions().mirror) { - //Randomize - std::array swaps = { 0,1,2,3 }; - - std::shuffle(swaps.begin(), swaps.end(), std::default_random_engine((int)(1000 * g_application->GetAppTime()))); - - bool unchanged = true; - for (int i = 0; i < 4; i++) - { - if (swaps[i] != i) - { - unchanged = false; - break; - } - } - bool flipFx = false; - - if (unchanged) - { - flipFx = true; - } - else - { - std::srand((int)(1000 * g_application->GetAppTime())); - flipFx = (std::rand() % 2) == 1; - } - - const Vector chartObjects = m_playback.GetBeatmap().GetLinearObjects(); - for (ObjectState* currentobj : chartObjects) - { - if (currentobj->type == ObjectType::Single || currentobj->type == ObjectType::Hold) - { - ButtonObjectState* bos = (ButtonObjectState*)currentobj; - if (bos->index < 4) - { - bos->index = swaps[bos->index]; - } - else if (flipFx) - { - bos->index = (bos->index - 3) % 2; - bos->index += 4; - } - } - } - - } - - if (GetPlaybackOptions().mirror) - { - int buttonSwaps[] = { 3,2,1,0,5,4 }; - - const Vector chartObjects = m_playback.GetBeatmap().GetLinearObjects(); - for (ObjectState* currentobj : chartObjects) - { - if (currentobj->type == ObjectType::Single || currentobj->type == ObjectType::Hold) - { - ButtonObjectState* bos = (ButtonObjectState*)currentobj; - bos->index = buttonSwaps[bos->index]; - } - else if (currentobj->type == ObjectType::Laser) - { - LaserObjectState* los = (LaserObjectState*)currentobj; - los->index = (los->index + 1) % 2; - for (size_t i = 0; i < 2; i++) - { - los->points[i] = fabsf(los->points[i] - 1.0f); - } - } - } + m_beatmap->Shuffle((int)(1000 * g_application->GetAppTime()), GetPlaybackOptions().random, GetPlaybackOptions().mirror); } if (m_practiceSetupDialog) @@ -936,19 +830,18 @@ class Game_Impl : public Game RenderState rs = m_camera.CreateRenderState(true); // Draw BG first - if(m_background) - m_background->Render(deltaTime); + if (m_background) + { + m_background->Render(deltaTime * m_playback.GetScrollSpeed()); + } // Main render queue RenderQueue renderQueue(g_gl, rs); // Get objects in range - MapTime msViewRange = m_playback.ViewDistanceToDuration(m_track->GetViewRange()); - if (m_speedMod == SpeedMods::CMod) - { - msViewRange = 480000.0 / m_playback.cModSpeed; - } - m_currentObjectSet = m_playback.GetObjectsInRange(msViewRange); + m_currentObjectSet.clear(); + m_playback.GetObjectsInViewRange(m_track->GetViewRange(), m_currentObjectSet); + // Sort objects to draw // fx holds -> bt holds -> fx chips -> bt chips m_currentObjectSet.Sort([](const TObjectState* a, const TObjectState* b) @@ -1252,7 +1145,7 @@ class Game_Impl : public Game } virtual void PermanentlyHideTickObject(MapTime t, int lane) override { - ObjectState* obj = m_playback.GetFirstButtonOrHoldAfterTime(t, lane); + const ObjectState* obj = m_playback.GetFirstButtonOrHoldAfterTime(t, lane); m_permanentlyHiddenObjects.insert(obj); } @@ -1280,17 +1173,9 @@ class Game_Impl : public Game { // Select the correct first object to set the intial playback position // if it starts before a certain time frame, the song starts at a negative time (lead-in) - ObjectState* const* firstObj = &m_beatmap->GetLinearObjects().front(); - for (; firstObj != &m_beatmap->GetLinearObjects().back(); ++firstObj) - { - if ((*firstObj)->type == ObjectType::Event) continue; - if ((*firstObj)->time < m_playOptions.range.begin) continue; - - break; - } const MapTime beginTime = m_playOptions.range.begin; - const MapTime firstObjectTime = (*firstObj)->time; + const MapTime firstObjectTime = m_beatmap->GetFirstObjectTime(beginTime); return std::min(beginTime, firstObjectTime - GetAudioLeadIn()); } @@ -1548,8 +1433,6 @@ class Game_Impl : public Game m_currentTiming = &m_playback.GetCurrentTimingPoint(); // Update song info display - ObjectState *const* lastObj = &m_beatmap->GetLinearObjects().back(); - if (m_multiplayer != nullptr) m_multiplayer->PerformScoreTick(m_scoring, m_lastMapTime); @@ -1959,6 +1842,7 @@ class Game_Impl : public Game textPos.y += RenderText(Utility::Sprintf("Track Zoom Top: %f", m_camera.pLanePitch), textPos).y; textPos.y += RenderText(Utility::Sprintf("Track Zoom Bottom: %f", m_camera.pLaneZoom), textPos).y; + textPos.y += RenderText(Utility::Sprintf("Scroll Speed: %f", m_playback.GetScrollSpeed()), textPos).y; Vector2 buttonStateTextPos = Vector2(g_resolution.x - 200.0f, 100.0f); RenderText(g_input.GetControllerStateString(), buttonStateTextPos); @@ -2230,16 +2114,16 @@ class Game_Impl : public Game } } - void OnTimingPointChanged(TimingPoint* tp) + void OnTimingPointChanged(Beatmap::TimingPointsIterator tp) { m_hispeed = m_modSpeed / tp->GetBPM(); } - void OnTimingPointChangedChallenge(TimingPoint* tp) + void OnTimingPointChangedChallenge(Beatmap::TimingPointsIterator tp) { CheckChallengeHispeed(tp->GetBPM()); } - void OnLaneToggleChanged(LaneHideTogglePoint* tp) + void OnLaneToggleChanged(Beatmap::LaneTogglePointsIterator tp) { // Calculate how long the transition should be in seconds double duration = m_currentTiming->beatDuration * 4.0f * (tp->duration / 192.0f) * 0.001f; @@ -2421,8 +2305,7 @@ class Game_Impl : public Game return; } - ObjectState* const* lastObj = &m_beatmap->GetLinearObjects().back(); - MapTime timePastEnd = m_lastMapTime - (*lastObj)->time; + const MapTime timePastEnd = m_lastMapTime - m_beatmap->GetLastObjectTimeIncludingEvents(); if (timePastEnd < 0) m_manualExit = true; @@ -2555,12 +2438,8 @@ class Game_Impl : public Game // Skips ahead to the right before the first object in the map bool SkipIntro() { - ObjectState* const* firstObj = &m_beatmap->GetLinearObjects().front(); - while ((*firstObj)->type == ObjectType::Event && firstObj != &m_beatmap->GetLinearObjects().back()) - { - firstObj++; - } - MapTime skipTime = (*firstObj)->time - 1000; + const MapTime skipTime = m_beatmap->GetFirstObjectTime(0) - 1000; + if (skipTime > m_lastMapTime) { // In multiplayer mode we have to stay synced @@ -2572,19 +2451,19 @@ class Game_Impl : public Game } return false; } + // Skips ahead at the end to the score screen void SkipOutro() { // Just to be sure - if (m_beatmap->GetLinearObjects().empty()) + if (!m_beatmap->HasObjectState()) { FinishGame(); return; } // Check if last object has passed - ObjectState* const* lastObj = &m_beatmap->GetLinearObjects().back(); - MapTime timePastEnd = m_lastMapTime - (*lastObj)->time; + const MapTime timePastEnd = m_lastMapTime - m_beatmap->GetLastObjectTimeIncludingEvents(); if (timePastEnd > 250) { FinishGame(); @@ -2919,7 +2798,7 @@ class Game_Impl : public Game Track& track = this->GetTrack(); float viewRange = track.GetViewRange(); - float trackScale = (this->GetPlayback().DurationToViewDistanceAtTime(time, duration) / viewRange); + float trackScale = (this->GetPlayback().ToViewDistance(time, duration) / viewRange); float scale = trackScale * track.trackLength; lua_pushnumber(L, scale); return 1; diff --git a/Main/src/LaserTrackBuilder.cpp b/Main/src/LaserTrackBuilder.cpp index 6516ccc2d..0337b5d6c 100644 --- a/Main/src/LaserTrackBuilder.cpp +++ b/Main/src/LaserTrackBuilder.cpp @@ -27,7 +27,7 @@ Mesh LaserTrackBuilder::GenerateTrackMesh(class BeatmapPlayback& playback, Laser Mesh newMesh = MeshRes::Create(m_gl); - float length = playback.DurationToViewDistanceAtTime(laser->time, laser->duration); + const float length = playback.ToViewDistance(laser->time, laser->duration); if((laser->flags & LaserObjectState::flag_Instant) != 0) // Slam segment { @@ -61,7 +61,7 @@ Mesh LaserTrackBuilder::GenerateTrackMesh(class BeatmapPlayback& playback, Laser }// else ------> // Generate positions for middle top and bottom - float slamLength = playback.DurationToViewDistanceAtTime(laser->time, slamDuration) * laserLengthScale; + float slamLength = playback.ToViewDistance(laser->time, slamDuration) * laserLengthScale; float halfLength = slamLength * 0.5; Rect3D centerMiddle = Rect3D(left, slamLength + halfLength, right, -halfLength); @@ -141,7 +141,7 @@ Mesh LaserTrackBuilder::GenerateTrackMesh(class BeatmapPlayback& playback, Laser if(laser->prev && (laser->prev->flags & LaserObjectState::flag_Instant) != 0) { // Previous slam length - prevLength = playback.DurationToViewDistanceAtTime(laser->prev->time, slamDuration) * laserLengthScale; + prevLength = playback.ToViewDistance(laser->prev->time, slamDuration) * laserLengthScale; } Vector2 points[2]; @@ -233,11 +233,11 @@ Mesh LaserTrackBuilder::GenerateTrackExit(class BeatmapPlayback& playback, Laser float prevLength = 0.0f; if((laser->flags & LaserObjectState::flag_Instant) != 0) { - prevLength = playback.DurationToViewDistanceAtTime(laser->time, slamDuration) * laserLengthScale; + prevLength = playback.ToViewDistance(laser->time, slamDuration) * laserLengthScale; } else { - prevLength = playback.DurationToViewDistanceAtTime(laser->time, laser->duration) * laserLengthScale; + prevLength = playback.ToViewDistance(laser->time, laser->duration) * laserLengthScale; } Vector verts; diff --git a/Main/src/MultiplayerScreen.cpp b/Main/src/MultiplayerScreen.cpp index 0d78222b9..488e0d6d2 100644 --- a/Main/src/MultiplayerScreen.cpp +++ b/Main/src/MultiplayerScreen.cpp @@ -580,7 +580,7 @@ void MultiplayerScreen::m_changeDifficulty(int offset) } -void MultiplayerScreen::GetMapBPMForSpeed(String path, struct MultiplayerBPMInfo& info) +void MultiplayerScreen::GetMapBPMForSpeed(String path, struct MultiplayerBPMInfo& info) { path = Path::Normalize(path); if (!Path::FileExists(path)) @@ -592,93 +592,23 @@ void MultiplayerScreen::GetMapBPMForSpeed(String path, struct MultiplayerBPMInf } // Load map - Beatmap* newMap = new Beatmap(); + Beatmap newMap; File mapFile; if (!mapFile.OpenRead(path)) { Logf("Could not read path for beatmap: %s", Logger::Severity::Error, path); - delete newMap; info = { 0, 0, 0, 0 }; return; } + FileReader reader(mapFile); - if (!newMap->Load(reader)) + if (!newMap.Load(reader)) { - delete newMap; info = { 0, 0, 0, 0 }; return; } - // Most of this code is copied from Game.cpp to match its calculations - - double useBPM = -1; - - const BeatmapSettings& mapSettings = newMap->GetMapSettings(); - - info.start = newMap->GetLinearTimingPoints().front()->GetBPM(); - - ObjectState* const* lastObj = &newMap->GetLinearObjects().back(); - while ((*lastObj)->type == ObjectType::Event && lastObj != &newMap->GetLinearObjects().front()) - { - lastObj--; - } - - MapTime lastObjectTime = (*lastObj)->time; - if ((*lastObj)->type == ObjectType::Hold) - { - HoldObjectState* lastHold = (HoldObjectState*)(*lastObj); - lastObjectTime += lastHold->duration; - } - else if ((*lastObj)->type == ObjectType::Laser) - { - LaserObjectState* lastHold = (LaserObjectState*)(*lastObj); - lastObjectTime += lastHold->duration; - } - - { - Map bpmDurations; - const Vector& timingPoints = newMap->GetLinearTimingPoints(); - MapTime lastMT = mapSettings.offset; - MapTime largestMT = -1; - double lastBPM = -1; - - info.min = -1; - info.max = -1; - - for (TimingPoint* tp : timingPoints) - { - double thisBPM = tp->GetBPM(); - - if (info.max == -1 || thisBPM > info.max) - info.max = thisBPM; - - if (info.min == -1 || thisBPM < info.min) - info.min = thisBPM; - - if (!bpmDurations.count(lastBPM)) - { - bpmDurations[lastBPM] = 0; - } - MapTime timeSinceLastTP = tp->time - lastMT; - bpmDurations[lastBPM] += timeSinceLastTP; - if (bpmDurations[lastBPM] > largestMT) - { - useBPM = lastBPM; - largestMT = bpmDurations[lastBPM]; - } - lastMT = tp->time; - lastBPM = thisBPM; - } - bpmDurations[lastBPM] += lastObjectTime - lastMT; - - if (bpmDurations[lastBPM] > largestMT) - { - useBPM = lastBPM; - } - info.mode = useBPM; - } - - delete newMap; + newMap.GetBPMInfo(info.start, info.min, info.max, info.mode); } ChartIndex* MultiplayerScreen::GetCurrentSelectedChart() const diff --git a/Main/src/Scoring.cpp b/Main/src/Scoring.cpp index 19f8ec0c6..8494f218f 100755 --- a/Main/src/Scoring.cpp +++ b/Main/src/Scoring.cpp @@ -1391,13 +1391,13 @@ MapTotals Scoring::CalculateMapTotals() const MapTotals ret = { 0, 0, 0 }; const Beatmap& map = m_playback->GetBeatmap(); - Set processedLasers; + Set processedLasers; assert(m_playback); - auto& objects = map.GetLinearObjects(); - for (auto& _obj : objects) + + for (const auto& _obj : map.GetObjectStates()) { - MultiObjectState* obj = *_obj; + const MultiObjectState* obj = *_obj; const TimingPoint* tp = m_playback->GetTimingPointAt(obj->time); if (obj->type == ObjectType::Single) { @@ -1413,7 +1413,7 @@ MapTotals Scoring::CalculateMapTotals() const } else if (obj->type == ObjectType::Laser) { - LaserObjectState* laserRoot = obj->laser.GetRoot(); + const LaserObjectState* laserRoot = obj->laser.GetRoot(); // Don't evaluate ticks for every segment, only for entire chains of segments if (!processedLasers.Contains(laserRoot)) diff --git a/Main/src/Track.cpp b/Main/src/Track.cpp index a46e0ebab..54d30fc4e 100644 --- a/Main/src/Track.cpp +++ b/Main/src/Track.cpp @@ -290,41 +290,8 @@ void Track::Tick(class BeatmapPlayback& playback, float deltaTime) trackViewRange = Vector2((float)currentTime, 0.0f); trackViewRange.y = trackViewRange.x + GetViewRange(); - // Update ticks separating bars to draw - double tickTime = (double)currentTime; - MapTime rangeEnd = currentTime + playback.ViewDistanceToDuration(m_viewRange); - const TimingPoint* tp = playback.GetTimingPointAt((MapTime)tickTime); - double stepTime = tp->GetBarDuration(); // Every xth note based on signature - - // Overflow on first tick - double firstOverflow = fmod((double)tickTime - tp->time, stepTime); - if(fabs(firstOverflow) > 1) - tickTime -= firstOverflow; - m_barTicks.clear(); - - // Add first tick - m_barTicks.Add(playback.TimeToViewDistance((MapTime)tickTime)); - - while (tickTime < rangeEnd) - { - double next = tickTime + stepTime; - - const TimingPoint* tpNext = playback.GetTimingPointAt((MapTime)tickTime); - if(tpNext != tp) - { - tp = tpNext; - tickTime = tp->time; - stepTime = tp->GetBarDuration(); // Every xth note based on signature - } - else - { - tickTime = next; - } - - // Add tick - m_barTicks.Add(playback.TimeToViewDistance((MapTime)tickTime)); - } + playback.GetBarPositionsInViewRange(m_viewRange, m_barTicks); // Update track hide status m_trackHide += m_trackHideSpeed * deltaTime; @@ -441,9 +408,16 @@ void Track::DrawObjectState(RenderQueue& rq, class BeatmapPlayback& playback, Ob { // Calculate height based on time on current track float viewRange = GetViewRange(); - float position = playback.TimeToViewDistance(obj->time) / viewRange; float glow = 0.0f; + const bool dontUseScrollSpeedForPos = + obj->type == ObjectType::Hold ? ((MultiObjectState*)obj)->hold.GetRoot()->time <= playback.GetLastTime() + : obj->type == ObjectType::Laser ? obj->time <= playback.GetLastTime() + : false; + + float position = dontUseScrollSpeedForPos ? playback.TimeToViewDistanceIgnoringScrollSpeed(obj->time) : playback.TimeToViewDistance(obj->time); + position /= viewRange; + if(obj->type == ObjectType::Single || obj->type == ObjectType::Hold) { bool isHold = obj->type == ObjectType::Hold; @@ -519,7 +493,25 @@ void Track::DrawObjectState(RenderQueue& rq, class BeatmapPlayback& playback, Ob float scale; if(isHold) // Hold Note? { - float trackScale = (playback.DurationToViewDistanceAtTime(mobj->time, mobj->hold.duration) / viewRange) / length; + float trackScale = 0.0f; + if (dontUseScrollSpeedForPos) + { + if (mobj->time + mobj->hold.duration <= playback.GetLastTime()) + { + trackScale = playback.ToViewDistanceIgnoringScrollSpeed(mobj->time, mobj->hold.duration); + } + else + { + const float remainingDistance = playback.TimeToViewDistance(mobj->time + mobj->hold.duration); + trackScale = Math::Max(0.0f, remainingDistance) - playback.TimeToViewDistanceIgnoringScrollSpeed(mobj->time); + } + } + else + { + trackScale = playback.ToViewDistance(mobj->time, mobj->hold.duration); + } + + trackScale /= viewRange * length; scale = trackScale * trackLength; params.SetParameter("trackScale", trackScale); diff --git a/Main/stdafx.h b/Main/stdafx.h index 9f922c1b9..1c6da28e2 100644 --- a/Main/stdafx.h +++ b/Main/stdafx.h @@ -28,6 +28,7 @@ #include #include +#include #include #include #include diff --git a/Shared/include/Shared/Math.hpp b/Shared/include/Shared/Math.hpp index a1d1656fc..8f582c399 100644 --- a/Shared/include/Shared/Math.hpp +++ b/Shared/include/Shared/Math.hpp @@ -1,7 +1,10 @@ #pragma once + #include #include +#include + namespace Math { // Floating point PI constant @@ -40,6 +43,13 @@ namespace Math return v; } + template + static constexpr std::enable_if_t::value, T> Lerp(T a, T b, T t) noexcept + { + // Not the spec-confirming lerp, but good enough + return t == 0 ? a : t == 1 ? b : a + t * (b - a); + } + // Templated Greatest common divisor template static T GCD(T a, T b) diff --git a/Shared/stdafx.h b/Shared/stdafx.h index 332d1f37d..303e4ee29 100644 --- a/Shared/stdafx.h +++ b/Shared/stdafx.h @@ -14,4 +14,7 @@ #include "Shared/Types.hpp" #include -#include \ No newline at end of file +#include +#include +#include +#include \ No newline at end of file diff --git a/Tests.Game/src/TestBeatmap.cpp b/Tests.Game/src/TestBeatmap.cpp index e4704cd88..7896fa2e3 100644 --- a/Tests.Game/src/TestBeatmap.cpp +++ b/Tests.Game/src/TestBeatmap.cpp @@ -10,25 +10,24 @@ static String testBeatmapPath = Path::Normalize("songs/love is insecurable/love_ // Song with speed changes static String testBeatmapPath1 = Path::Normalize("songs/soflan/Konran shoujo Soflan-chan!!.ksh"); -Beatmap LoadTestBeatmap(const String& mapPath = testBeatmapPath) +static inline void LoadTestBeatmap(Beatmap& beatmap, const String& mapPath = testBeatmapPath) { - Beatmap beatmap; File file; TestEnsure(file.OpenRead(mapPath)); FileReader reader(file); TestEnsure(beatmap.Load(reader)); - return beatmap; } Test("Beatmap.v160") { - Beatmap map = LoadTestBeatmap(Path::Normalize("D:\\KShoot/songs/Other/CHNLDiVR/exh.ksh")); + Beatmap map; + LoadTestBeatmap(map, Path::Normalize("D:\\KShoot/songs/Other/CHNLDiVR/exh.ksh")); // Should have bitcrush effect bool haveBitc = false; - for(auto obj : map.GetLinearObjects()) + for(auto& it : map.GetObjectStates()) { - MultiObjectState* mobj = *obj; + MultiObjectState* mobj = *(it.get()); if(mobj->type == ObjectType::Hold) { if(mobj->hold.effectType == EffectType::Bitcrush) @@ -43,7 +42,8 @@ Test("Beatmap.v160") // Test loading fo map + metadata without errors Test("Beatmap.Loading") { - Beatmap beatmap = LoadTestBeatmap(); + Beatmap beatmap; + LoadTestBeatmap(beatmap); BeatmapSettings settings = beatmap.GetMapSettings(); Logf("Artist: %s\n\t\tTitle: %s\n\t\tEffector: %s\n\t\tIllust: %s", Logger::Severity::Info, settings.artist, settings.title, settings.effector, settings.illustrator); @@ -54,7 +54,8 @@ Test("Beatmap.Loading") // Test 4/4 single bpm map Test("Beatmap.Playback") { - Beatmap beatmap = LoadTestBeatmap(); + Beatmap beatmap; + LoadTestBeatmap(beatmap); String mapRootPath = Path::RemoveLast(testBeatmapPath); class Player : public TestMusicPlayer @@ -70,10 +71,10 @@ Test("Beatmap.Playback") Init(songPath, settings.previewOffset); playback = BeatmapPlayback(beatmap); playback.Reset(settings.previewOffset); - playback.OnTimingPointChanged.AddLambda([](TimingPoint* obj) + playback.OnTimingPointChanged.AddLambda([](Beatmap::TimingPointsIterator it) { - float bpm = (float)obj->GetBPM(); - Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, obj->numerator, obj->denominator); + float bpm = (float)it->GetBPM(); + Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, it->numerator, it->denominator); }); } void Update(float dt) override @@ -94,7 +95,8 @@ Test("Beatmap.Playback") // Different map with many BPM changes Test("Beatmap.BPMChanges") { - Beatmap beatmap = LoadTestBeatmap(testBeatmapPath1); + Beatmap beatmap; + LoadTestBeatmap(beatmap, testBeatmapPath1); String mapRootPath = Path::RemoveLast(testBeatmapPath1); class Player : public TestMusicPlayer @@ -110,10 +112,10 @@ Test("Beatmap.BPMChanges") Init(songPath, settings.previewOffset); playback = BeatmapPlayback(beatmap); playback.Reset(settings.previewOffset); - playback.OnTimingPointChanged.AddLambda([](TimingPoint* obj) + playback.OnTimingPointChanged.AddLambda([](Beatmap::TimingPointsIterator it) { - float bpm = (float)obj->GetBPM(); - Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, obj->numerator, obj->denominator); + float bpm = (float) it->GetBPM(); + Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, it->numerator, it->denominator); }); } void Update(float dt) override @@ -134,7 +136,8 @@ Test("Beatmap.BPMChanges") // Test applying effects over maps Test("Beatmap.DoubleFilter") { - Beatmap beatmap = LoadTestBeatmap(); + Beatmap beatmap; + LoadTestBeatmap(beatmap); String mapRootPath = Path::RemoveLast(testBeatmapPath); class Player : public TestMusicPlayer @@ -156,10 +159,9 @@ Test("Beatmap.DoubleFilter") playback = BeatmapPlayback(beatmap); playback.Reset(0); - playback.OnTimingPointChanged.AddLambda([](TimingPoint* obj) - { - float bpm = (float)obj->GetBPM(); - Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, obj->numerator, obj->denominator); + playback.OnTimingPointChanged.AddLambda([](Beatmap::TimingPointsIterator it) { + float bpm = (float)it->GetBPM(); + Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, it->numerator, it->denominator); }); playback.OnObjectEntered.AddLambda([&](ObjectState* obj) { if(obj->type == ObjectType::Laser) @@ -242,7 +244,8 @@ Test("Beatmap.DoubleFilter") // Test applying effects over maps Test("Beatmap.SingleFilter") { - Beatmap beatmap = LoadTestBeatmap(); + Beatmap beatmap; + LoadTestBeatmap(beatmap); String mapRootPath = Path::RemoveLast(testBeatmapPath); class Player : public TestMusicPlayer @@ -264,10 +267,9 @@ Test("Beatmap.SingleFilter") playback = BeatmapPlayback(beatmap); playback.Reset(0); - playback.OnTimingPointChanged.AddLambda([](TimingPoint* obj) - { - float bpm = (float)obj->GetBPM(); - Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, obj->numerator, obj->denominator); + playback.OnTimingPointChanged.AddLambda([](Beatmap::TimingPointsIterator it) { + float bpm = (float)it->GetBPM(); + Logf("T %.2f BPM %d/%d", Logger::Severity::Info, bpm, it->numerator, it->denominator); }); playback.OnObjectEntered.AddLambda([&](ObjectState* obj) { if(obj->type == ObjectType::Laser) diff --git a/Tests.Game/src/TestMusicPlayer.hpp b/Tests.Game/src/TestMusicPlayer.hpp index 7875dddcc..63435cfd8 100644 --- a/Tests.Game/src/TestMusicPlayer.hpp +++ b/Tests.Game/src/TestMusicPlayer.hpp @@ -1,6 +1,6 @@ #pragma once -class TestMusicPlayer +class TestMusicPlayer: public Unique { public: Audio* audio;