Permalink
Browse files

rewritten timekeeping routines for accuracy.

- timeline contains the precalculated timekeeping informations.
  subsequently `adjust_*` methods are now gone. (hurray!)
- timekeeping routine is now lag-resistant.
- `Pointer` interface is entirely redesigned for efficient use of
  revised timekeeping routine.
- reverse motion due to negative BPM now requires a special casing
  due to the algorithm change.
- measure bar past the end of the chart is not rendered now.
  • Loading branch information...
1 parent 6172102 commit f786f1094cc522b89c259a7b2cce3fc364f2f2fb @lifthrasiir lifthrasiir committed Jul 20, 2013
Showing with 718 additions and 453 deletions.
  1. +27 −17 src/format/bms/mod.rs
  2. +144 −34 src/format/obj.rs
  3. +257 −87 src/format/pointer.rs
  4. +155 −127 src/format/timeline.rs
  5. +135 −188 src/ui/player.rs
View
@@ -34,16 +34,10 @@
use std::uint;
use std::rand::*;
-pub use format::obj::{Lane, NLANES};
-pub use format::obj::{BGALayer, Layer1, Layer2, Layer3, PoorBGA, NLAYERS};
-pub use format::obj::{BPM};
-pub use format::obj::{Duration, Seconds, Measures};
-pub use format::obj::{Damage, GaugeDamage, InstantDeath};
-pub use format::obj::{ObjData, Deleted, Visible, Invisible, LNStart, LNDone, Bomb, BGM, SetBGA,
- SetBPM, Stop, End};
-pub use format::obj::{ObjQueryOps, ObjConvOps, Obj};
-pub use format::timeline::Timeline;
-pub use format::pointer::Pointer;
+use format::obj::*;
+use format::timeline::Timeline;
+use format::pointer::Pointer;
+
pub use format::bms::types::{Key, MAXKEY};
pub mod types;
@@ -258,6 +252,9 @@ pub fn parse_bms_from_reader<R:RngUtil>(f: @::std::io::Reader, r: &mut R) -> Res
// A list of unprocessed data lines. They have to be sorted with a stable algorithm and
// processed in the order of measure number. (C: `bmsline`)
let mut bmsline = ~[];
+ // A table of measure factors (#xxx02). They are eventually converted to `SetMeasureFactor`
+ // objects.
+ let mut shortens = ~[];
// A table of BPMs. Maps to BMS #BPMxx command. (C: `bpmtab`)
let mut bpmtab = ~[DefaultBPM, ..MAXKEY];
// A table of the length of scroll stoppers. Maps to BMS #STOP/#STP commands. (C: `stoptab`)
@@ -293,9 +290,9 @@ pub fn parse_bms_from_reader<R:RngUtil>(f: @::std::io::Reader, r: &mut R) -> Res
(parse::BmsStop(Key(i), dur), false) => { stoptab[i] = dur; }
(parse::BmsStp(pos, dur), false) => { builder.add(pos, Stop(dur)); }
- (parse::BmsShorten(measure, shorten), false) => {
- if shorten > 0.001 {
- builder.set_shorten(measure, shorten);
+ (parse::BmsShorten(measure, factor), false) => {
+ if factor > 0.0 {
+ shortens.grow_set(measure, &1.0, factor);
}
}
(parse::BmsData(measure, chan, data), false) => {
@@ -501,7 +498,7 @@ pub fn parse_bms_from_reader<R:RngUtil>(f: @::std::io::Reader, r: &mut R) -> Res
}
};
- // loops over the sorted bmslines
+ // loop over the sorted bmslines
::extra::sort::tim_sort(bmsline);
for bmsline.iter().advance |line| {
let measure = line.measure as float;
@@ -520,20 +517,33 @@ pub fn parse_bms_from_reader<R:RngUtil>(f: @::std::io::Reader, r: &mut R) -> Res
}
}
+ // insert an artificial `SetBGA` object at 0.0 if required
if poorbgafix {
builder.add(0.0, SetBGA(PoorBGA, Some(ImageRef(Key(0)))));
}
// fix the unterminated longnote
- let endt = bmsline.last_opt().map_default(0.0, |l| l.measure as float) + 1.0;
+ let nmeasures = bmsline.last_opt().map_default(0, |l| l.measure) + 1;
+ let endt = nmeasures as float;
for uint::range(0, NLANES) |i| {
if lastvis[i].is_some() || (!consecutiveln && lastln[i].is_some()) {
builder.add(endt, LNDone(Lane(i), None));
}
}
- // mark the end of the chart
- builder.add(endt + 1.0, End);
+ // convert shortens to objects and insert measure bars
+ shortens.grow_set(nmeasures, &1.0, 1.0); // so we always have a normal measure at the end
+ let mut prevfactor = 1.0;
+ for shortens.iter().enumerate().advance |(measure, &factor)| {
+ builder.add(measure as float, MeasureBar);
+ if prevfactor != factor {
+ builder.add(measure as float, SetMeasureFactor(factor));
+ prevfactor = factor;
+ }
+ }
+
+ // set the end of the chart (no measure bar at this position)
+ builder.set_end(endt + 1.0);
let timeline = builder.build();
Ok(Bms { bmspath: ~"",
View
@@ -6,14 +6,14 @@
/// A game play element mapped to the single input element (for example, button) and the screen
/// area (henceforth "lane").
-#[deriving(Eq)]
+#[deriving(Eq,Clone)]
pub struct Lane(uint);
/// The maximum number of lanes. (C: `NNOTECHANS`)
pub static NLANES: uint = 72;
/// BGA layers. (C: `enum BGA_type`)
-#[deriving(Eq,ToStr)]
+#[deriving(Eq,ToStr,Clone)]
pub enum BGALayer {
/// The lowest layer. BMS channel #04. (C: `BGA_LAYER`)
Layer1 = 0,
@@ -32,40 +32,40 @@ pub static NLAYERS: uint = 4;
/// Beats per minute. Used as a conversion factor between the time position and actual time
/// in BMS.
-#[deriving(Eq,ToStr)]
+#[deriving(Eq,ToStr,Clone)]
pub struct BPM(float);
impl BPM {
- /// Converts a measure to a millisecond. (C: `MEASURE_TO_MSEC`)
- pub fn measure_to_msec(self, measure: float) -> float { measure * 240000.0 / *self }
+ /// Converts a measure to a second.
+ pub fn measure_to_sec(self, measure: float) -> float { measure * 240.0 / *self }
- /// Converts a millisecond to a measure. (C: `MSEC_TO_MEASURE`)
- pub fn msec_to_measure(self, msec: float) -> float { msec * *self / 240000.0 }
+ /// Converts a second to a measure.
+ pub fn sec_to_measure(self, sec: float) -> float { sec * *self / 240.0 }
}
/// A duration from the particular point. It may be specified in measures or seconds. Used in
/// the `Stop` object.
-#[deriving(Eq,ToStr)]
+#[deriving(Eq,ToStr,Clone)]
pub enum Duration { Seconds(float), Measures(float) }
impl Duration {
- /// Calculates the actual milliseconds from the current BPM.
- pub fn to_msec(&self, bpm: BPM) -> float {
+ /// Calculates the actual seconds from the current BPM.
+ pub fn to_sec(&self, bpm: BPM) -> float {
match *self {
- Seconds(secs) => secs * 1000.0,
- Measures(measures) => bpm.measure_to_msec(measures)
+ Seconds(secs) => secs,
+ Measures(measures) => bpm.measure_to_sec(measures)
}
}
}
/// A damage value upon the MISS grade. Normally it is specified in percents of the full gauge
/// (as in `MAXGAUGE`), but sometimes it may cause an instant death. Used in the `Bomb` object
/// (normal note objects have a fixed value).
-#[deriving(Eq,ToStr)]
+#[deriving(Eq,ToStr,Clone)]
pub enum Damage { GaugeDamage(float), InstantDeath }
/// A data for objects (or object-like effects). Does not include the time information.
-#[deriving(Eq)]
+#[deriving(Eq,Clone)]
pub enum ObjData<SoundRef,ImageRef> {
/// Deleted object. Only used during various processing.
Deleted,
@@ -96,20 +96,29 @@ pub enum ObjData<SoundRef,ImageRef> {
* can be shared among multiple layers.
*/
SetBGA(BGALayer, Option<ImageRef>),
- /// Sets the BPM. Negative BPM causes the chart scrolls backwards (and implicitly signals
- /// the end of the chart). (C: `BPM_CHANNEL`)
+ /// Sets the BPM. Negative BPM causes the chart scrolls backwards. Zero BPM causes the chart
+ /// immediately terminates. In both cases, the chart is considered unfinished if there are
+ /// remaining gradable objects. (C: `BPM_CHANNEL`)
SetBPM(BPM),
- /// Stops the scroll of the chart for given duration ("scroll stopper" hereafter).
- /// (C: `STOP_CHANNEL`)
+ /// Stops the scroll of the chart for given duration ("scroll stopper" hereafter). The duration,
+ /// if specified in measures, is not affected by the measure scaling factor. (C: `STOP_CHANNEL`)
Stop(Duration),
+ /// Restarts the scroll of the chart. This object is a no-op, but it is used to keep
+ /// the linear relation between time and position axes.
+ StopEnd,
+ /// Sets the measure scaling factor, which is a ratio of the interval in the virtual position
+ /// (e.g. as specified by the BMS creators) and the interval in the actual position.
+ /// This can be ignored for the game play, but we still keep this relation since the virtual
+ /// position is often directly used to refer certain point in the chart.
+ SetMeasureFactor(float),
+ /// Start of the measure, where the measure bar is drawn. This is derived from
+ /// `SetMeasureFactor` but made into the separate object as an optimization.
+ MeasureBar,
/// Marks the logical end of the chart. This is also useful to extend the chart without
/// inserting any dummy object after the end of the song. This object is otherwise a no-op,
- /// and expected to be the last object in the chart if present.
+ /// but it should be the last object in the chart and should be placed in the different
+ /// position from the next-to-last object (see `format::pointer` for rationale).
End,
-
- // XXX
- //SetShortenFactor(float),
- //MeasureBar, // ???
}
impl<S:ToStr,I:ToStr> ToStr for ObjData<S,I> {
@@ -144,6 +153,10 @@ impl<S:ToStr,I:ToStr> ToStr for ObjData<S,I> {
fmt!("Stop(%fs)", secs),
Stop(Measures(measures)) =>
fmt!("Stop(%f)", measures),
+ StopEnd => ~"StopEnd",
+ SetMeasureFactor(factor) =>
+ fmt!("SetMeasureFactor(%f)", factor),
+ MeasureBar => ~"MeasureBar",
End => ~"End",
}
}
@@ -205,6 +218,12 @@ pub trait ObjQueryOps<SoundRef:Copy,ImageRef:Copy> {
pub fn is_setbpm(&self) -> bool;
/// Returns true if the data is a scroll stopper. (C: `obj->chan == STOP_CHANNEL`)
pub fn is_stop(&self) -> bool;
+ /// Returns true if the data is the end of a scroll stopper.
+ pub fn is_stopend(&self) -> bool;
+ /// Returns true if the data is a change in the measure scaling factor.
+ pub fn is_setmeasurefactor(&self) -> bool;
+ /// Returns true if the data is a measure bar.
+ pub fn is_measurebar(&self) -> bool;
/// Returns true if the data is an end mark.
pub fn is_end(&self) -> bool;
@@ -314,6 +333,18 @@ impl<S:Copy,I:Copy,T:ToObjData<S,I>> ObjQueryOps<S,I> for T {
match self.to_obj_data() { Stop(*) => true, _ => false }
}
+ pub fn is_stopend(&self) -> bool {
+ match self.to_obj_data() { StopEnd => true, _ => false }
+ }
+
+ pub fn is_setmeasurefactor(&self) -> bool {
+ match self.to_obj_data() { SetMeasureFactor(*) => true, _ => false }
+ }
+
+ pub fn is_measurebar(&self) -> bool {
+ match self.to_obj_data() { MeasureBar => true, _ => false }
+ }
+
pub fn is_end(&self) -> bool {
match self.to_obj_data() { End => true, _ => false }
}
@@ -423,21 +454,100 @@ impl<S:Copy,I:Copy,T:WithObjData<S,I>+Copy> ObjConvOps<S,I> for T {
}
}
-/// Game play data associated to the time axis. It contains both objects (which are also
-/// associated to lanes) and object-like effects.
-#[deriving(Eq,ToStr)]
+/// Axes available to the objects. See `Obj` for more information.
+#[deriving(Eq,ToStr,Clone)]
+pub enum ObjAxis {
+ /// Virtual position.
+ VirtualPos = 0,
+ /// Actual position.
+ ActualPos = 1,
+ /// Virtual time.
+ VirtualTime = 2,
+ /// Actual time.
+ ActualTime = 3,
+}
+
+/// Object location per axis.
+#[deriving(Eq,ToStr,Clone)]
+pub struct ObjLoc<T> {
+ /// Virtual position in measures.
+ vpos: T,
+ /// Actual position in measures.
+ pos: T,
+ /// Virtual time in seconds. Can be a positive infinity if the chart scrolls backwards prior to
+ /// this object and this object should not be graded.
+ vtime: T,
+ /// Actual time in seconds. Can be a positive infinity if the chart scrolls backwards prior to
+ /// this object and this object should not be activated.
+ time: T,
+}
+
+impl<T:Copy+Ord> Ord for ObjLoc<T> {
+ fn lt(&self, other: &ObjLoc<T>) -> bool { self.time < other.time }
+ fn le(&self, other: &ObjLoc<T>) -> bool { self.time <= other.time }
+ fn ge(&self, other: &ObjLoc<T>) -> bool { self.time >= other.time }
+ fn gt(&self, other: &ObjLoc<T>) -> bool { self.time > other.time }
+}
+
+impl<T:Copy> Index<ObjAxis,T> for ObjLoc<T> {
+ fn index(&self, axis: &ObjAxis) -> T {
+ match *axis { VirtualPos => copy self.vpos, ActualPos => copy self.pos,
+ VirtualTime => copy self.vtime, ActualTime => copy self.time }
+ }
+}
+
+/**
+ * An object with precalculated position and time information.
+ *
+ * Sonorous has four distinct axes: virtual position, actual position, virtual time and actual time.
+ * Positions have a unit of measures, times have a unit of seconds. Specifically:
+ *
+ * - Virtual position is what the chart file originally specified, and also what the player and
+ * creator actually perceive as the "measure". This is used purely for user convenience.
+ * - Actual position is a position after the measure scaling factor is applied. It linearly relates
+ * to the relative position of game elements (the proportional factor being the "play speed").
+ * - Virtual time is related to the actual position by BPM. The grading procedure uses a distance
+ * between the gradable object's virtual time and the current virtual time (and so-called "grading
+ * area" is also defined in terms of virtual time) so the rapid change of BPM does not affect
+ * the grading. Virtual time literally *stops* when `Stop` is activated, so objects close to
+ * `Stop` have natural grading areas based on the chart appearance.
+ * - Actual time is when the object is actually activated (played or overlapped with the grading
+ * line), and related to the virtual time by `Stop` objects.
+ *
+ * First three axes can *stop* while the actual time progresses. Axes are linearly related between
+ * consecutive objects. The proportional factor should be non-negative; `SetBPM` with a negative
+ * BPM seems to be an exception, but it actually makes the proportional factor infinite.
+ *
+ * The following table illustrates various situations possible with this model.
+ *
+ * ~~~~
+ * vpos pos vtime time data
+ * ------ ------ ------ ------ ----------------
+ * -inf -inf -inf -inf SetBPM(120) (not actual object, derived from `timeline.initbpm`)
+ * 0.00 0.00 0.00 0.00 MeasureBar
+ * 1.00 1.00 2.00 2.00 MeasureBar, SetMeasureFactor(0.5)
+ * 2.00 1.50 3.00 3.00 MeasureBar, SetMeasureFactor(1)
+ * 2.50 2.00 4.00 4.00 SetBPM(240)
+ * 3.00 2.50 4.50 4.50 MeasureBar, SetBPM(24000)
+ * 4.00 3.50 4.51 4.51 MeasureBar, Stop(Seconds(1.48))
+ * 4.00 3.50 4.51 5.99 StopEnd
+ * 5.00 4.50 4.52 6.00 MeasureBar, SetBPM(-120)
+ * 6.00 5.50 +inf +inf MeasureBar
+ * ~~~~
+ */
+#[deriving(Eq,ToStr,Clone)]
pub struct Obj<SoundRef,ImageRef> {
- /// Time position in measures.
- time: float,
- /// Actual data.
+ /// Object location.
+ loc: ObjLoc<float>,
+ /// Associated object data.
data: ObjData<SoundRef,ImageRef>
}
impl<S:Copy,I:Copy> Ord for Obj<S,I> {
- fn lt(&self, other: &Obj<S,I>) -> bool { self.time < other.time }
- fn le(&self, other: &Obj<S,I>) -> bool { self.time <= other.time }
- fn ge(&self, other: &Obj<S,I>) -> bool { self.time >= other.time }
- fn gt(&self, other: &Obj<S,I>) -> bool { self.time > other.time }
+ fn lt(&self, other: &Obj<S,I>) -> bool { self.loc < other.loc }
+ fn le(&self, other: &Obj<S,I>) -> bool { self.loc <= other.loc }
+ fn ge(&self, other: &Obj<S,I>) -> bool { self.loc >= other.loc }
+ fn gt(&self, other: &Obj<S,I>) -> bool { self.loc > other.loc }
}
impl<S:Copy,I:Copy> ToObjData<S,I> for Obj<S,I> {
@@ -446,7 +556,7 @@ impl<S:Copy,I:Copy> ToObjData<S,I> for Obj<S,I> {
impl<S:Copy,I:Copy> WithObjData<S,I> for Obj<S,I> {
pub fn with_obj_data(&self, data: ObjData<S,I>) -> Obj<S,I> {
- Obj { time: self.time, data: data }
+ Obj { loc: self.loc.clone(), data: data }
}
}
Oops, something went wrong.

0 comments on commit f786f10

Please sign in to comment.