mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-10-12 01:24:27 +08:00
Compare commits
2
Commits
bd6ae99888
...
0b137effb5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b137effb5 | ||
|
|
b08af4deb7 |
@@ -9,9 +9,9 @@ use std::collections::HashMap;
|
|||||||
// note_bomb_3 5, note_bomb_5 6, note_bomb_9 7, note_slide 11, note_slide_event 12,
|
// note_bomb_3 5, note_bomb_5 6, note_bomb_9 7, note_slide 11, note_slide_event 12,
|
||||||
// note_slide_hold 13, with isHold(e) = e == 3 and isSlide(e) = e >= 11.
|
// note_slide_hold 13, with isHold(e) = e == 3 and isSlide(e) = e >= 11.
|
||||||
//
|
//
|
||||||
// SIF2 side: `type` only distinguishes an ordinary note (1) from a star/bomb note (3).
|
// SIF2 side: type 1 is a tap, 2 is a flick, and 3 is a skill/star note.
|
||||||
// LiveTimeController.ToMarkerType accepts 1..3 and maps anything else to None; type 2 exists in
|
// LiveTimeController.ToMarkerType accepts 1..3; stock charts happen not to
|
||||||
// the enum but appears in ZERO of the 2146 shipped charts, so nothing emits it here.
|
// use type 2, but imported Bandori charts do.
|
||||||
//
|
//
|
||||||
// A SLIDE is structural, not a type. MarkerData derives it from the parent/child chain:
|
// A SLIDE is structural, not a type. MarkerData derives it from the parent/child chain:
|
||||||
// IsSliderMarker chained and the child is on a DIFFERENT line -> a slide segment
|
// IsSliderMarker chained and the child is on a DIFFERENT line -> a slide segment
|
||||||
@@ -33,16 +33,19 @@ use std::collections::HashMap;
|
|||||||
// varies the blast width per effect; SIF2 has one bomb with one damage value, so the
|
// varies the blast width per effect; SIF2 has one bomb with one damage value, so the
|
||||||
// four collapse into type 3 and only the radius is lost. Previously only bomb_1 mapped
|
// four collapse into type 3 and only the radius is lost. Previously only bomb_1 mapped
|
||||||
// here and the three wider ones arrived as ordinary taps.
|
// here and the three wider ones arrived as ordinary taps.
|
||||||
// - effect 11/12/13 (slide) -> CHAINED across lanes, all type 1. Slides sharing a notes_level
|
// - effect 11/12/13/14 (slide) -> CHAINED across lanes, normally type 1. Slides sharing a notes_level
|
||||||
// form one run: sorted by time and linked parent -> child, so each link crosses lanes and
|
// form one run: sorted by time and linked parent -> child. Cross-lane links
|
||||||
// the client sees a slider. A run ends on effect 13 (slide hold), whose synthesized
|
// are sliders; same-lane links are hold segments. A run ends on effect 13 (slide hold), whose synthesized
|
||||||
// same-line tail then makes it IsSliderLongMarker — the slide settling into a hold the
|
// same-line tail then makes it IsSliderLongMarker — the slide settling into a hold the
|
||||||
// player releases. Verified against a real upload: every notes_level shared by more than
|
// player releases. Verified against a real upload: every notes_level shared by more than
|
||||||
// one note held exactly the slide-effect notes, each a monotonic sweep like
|
// one note held exactly the slide-effect notes, each a monotonic sweep like
|
||||||
// pos 9->8->7->6 with effects 11,11,11,13.
|
// pos 9->8->7->6 with effects 11,11,11,13.
|
||||||
// A lone slide with no chain partner stays a plain tap: a slider needs a cross-lane child.
|
// A lone slide with no chain partner stays a plain note. Same-lane links
|
||||||
// - effect 0 (random) and anything else unknown -> plain type 1. Every effect the game
|
// become hold segments; effect 14 keeps the final point as a flick.
|
||||||
// actually defines is covered above, so this is only a floor for hand-authored charts.
|
// - effects 8/14 are custom-song extensions for a flick and a slide-end flick (type 2).
|
||||||
|
// Effect 9 is a skill note (type 3); effect 10 is a hold with a flick tail.
|
||||||
|
// The stock SIF1 vocabulary does not use 8/9/10/14.
|
||||||
|
// - effect 0 (random) and anything else unknown -> plain type 1.
|
||||||
// - notes_attribute is dropped (SIF2 has no per-note attribute). notes_level is consumed as
|
// - notes_attribute is dropped (SIF2 has no per-note attribute). notes_level is consumed as
|
||||||
// the chain id above and not emitted.
|
// the chain id above and not emitted.
|
||||||
// - ids are sequential from 1 in time order. num is the spawn group: the dummy
|
// - ids are sequential from 1 in time order. num is the spawn group: the dummy
|
||||||
@@ -153,7 +156,7 @@ struct WorkNote {
|
|||||||
// LiveModel.NoteEffect.isHold, widened to note_slide_hold: both carry a duration in
|
// LiveModel.NoteEffect.isHold, widened to note_slide_hold: both carry a duration in
|
||||||
// effect_value (notes.lua isTimeOver adds effect_value for note_hold and note_slide_hold alike).
|
// effect_value (notes.lua isTimeOver adds effect_value for note_hold and note_slide_hold alike).
|
||||||
fn is_hold(effect: i64) -> bool {
|
fn is_hold(effect: i64) -> bool {
|
||||||
effect == 3 || effect == 13
|
effect == 3 || effect == 10 || effect == 13
|
||||||
}
|
}
|
||||||
|
|
||||||
// LiveModel.NoteEffect.isSlide
|
// LiveModel.NoteEffect.isSlide
|
||||||
@@ -231,7 +234,10 @@ pub fn transcode(beatmap: &JsonValue) -> Result<(JsonValue, i64), String> {
|
|||||||
work.push(WorkNote {
|
work.push(WorkNote {
|
||||||
time: timing,
|
time: timing,
|
||||||
line: position - 1,
|
line: position - 1,
|
||||||
kind: if is_bomb(effect) { 3 } else { 1 },
|
// Effects 8/14 are the custom-song interchange's flick and
|
||||||
|
// slide-end flick; 9 carries a Bandori skill note.
|
||||||
|
kind: if effect == 8 || effect == 14 { 2 }
|
||||||
|
else if effect == 9 || is_bomb(effect) { 3 } else { 1 },
|
||||||
parent: None,
|
parent: None,
|
||||||
child: None
|
child: None
|
||||||
});
|
});
|
||||||
@@ -249,7 +255,7 @@ pub fn transcode(beatmap: &JsonValue) -> Result<(JsonValue, i64), String> {
|
|||||||
work.push(WorkNote {
|
work.push(WorkNote {
|
||||||
time: timing + effect_value,
|
time: timing + effect_value,
|
||||||
line: position - 1,
|
line: position - 1,
|
||||||
kind: 1,
|
kind: if effect == 10 { 2 } else { 1 },
|
||||||
parent: Some(head),
|
parent: Some(head),
|
||||||
child: None
|
child: None
|
||||||
});
|
});
|
||||||
@@ -257,8 +263,8 @@ pub fn transcode(beatmap: &JsonValue) -> Result<(JsonValue, i64), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link each slide chain in time order. Consecutive members sit on different lines, which is
|
// Link each slide chain in time order. A cross-lane link becomes a slider
|
||||||
// exactly what makes SIF2 treat the run as a slider rather than a hold. A member that already
|
// and a same-lane link becomes a hold segment. A member that already
|
||||||
// has a child is a slide-hold, i.e. the end of the run, so the chain stops there — its tail
|
// has a child is a slide-hold, i.e. the end of the run, so the chain stops there — its tail
|
||||||
// stays its child and the cross-lane parent link makes it IsSliderLongMarker.
|
// stays its child and the cross-lane parent link makes it IsSliderLongMarker.
|
||||||
for (_, members) in chains.iter() {
|
for (_, members) in chains.iter() {
|
||||||
@@ -273,10 +279,6 @@ pub fn transcode(beatmap: &JsonValue) -> Result<(JsonValue, i64), String> {
|
|||||||
if work[a].child.is_some() || work[b].parent.is_some() {
|
if work[a].child.is_some() || work[b].parent.is_some() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if work[a].line == work[b].line {
|
|
||||||
// Same lane would read as a hold, not a slide; skip the link rather than lie
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
work[a].child = Some(b);
|
work[a].child = Some(b);
|
||||||
work[b].parent = Some(a);
|
work[b].parent = Some(a);
|
||||||
}
|
}
|
||||||
@@ -867,13 +869,32 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn undefined_effects_fall_back_to_taps() {
|
fn undefined_effects_fall_back_to_taps() {
|
||||||
// Not part of NoteEffect; a hand-authored chart must still transcode to something valid
|
for effect in [0, 15] {
|
||||||
for effect in [0, 8, 9, 10] {
|
|
||||||
let (chart, _) = transcode(&jzon::array![sif_note(1.0, 5, effect, 0.0)]).unwrap();
|
let (chart, _) = transcode(&jzon::array![sif_note(1.0, 5, effect, 0.0)]).unwrap();
|
||||||
assert_eq!(chart["notes"][1]["type"], 1, "effect {}", effect);
|
assert_eq!(chart["notes"][1]["type"], 1, "effect {}", effect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bandori_flick_skill_and_slide_flick() {
|
||||||
|
for (effect, kind) in [(8, 2), (9, 3), (14, 2)] {
|
||||||
|
let (chart, _) = transcode(&jzon::array![sif_note(1.0, 5, effect, 0.0)]).unwrap();
|
||||||
|
assert_eq!(chart["notes"][1]["type"], kind, "effect {}", effect);
|
||||||
|
}
|
||||||
|
let (chart, combo) = transcode(&jzon::array![
|
||||||
|
sif_slide(1.0, 7, 11, 0.0, 2),
|
||||||
|
sif_slide(1.3, 5, 14, 0.0, 2)
|
||||||
|
]).unwrap();
|
||||||
|
assert_eq!(combo, 2);
|
||||||
|
assert_eq!(chart["notes"][2]["type"], 2);
|
||||||
|
assert_eq!(chart["notes"][1]["child_id"], chart["notes"][2]["id"].clone());
|
||||||
|
|
||||||
|
let (hold, combo) = transcode(&jzon::array![sif_note(1.0, 5, 10, 0.5)]).unwrap();
|
||||||
|
assert_eq!(combo, 1);
|
||||||
|
assert_eq!(hold["notes"][1]["type"], 1);
|
||||||
|
assert_eq!(hold["notes"][2]["type"], 2);
|
||||||
|
}
|
||||||
|
|
||||||
// Lifted verbatim from a chart uploaded to the live server (custom song 10008, "Edelied",
|
// Lifted verbatim from a chart uploaded to the live server (custom song 10008, "Edelied",
|
||||||
// exported via /custom_song/download/10008) — a swipe run of note_slide into note_slide_hold,
|
// exported via /custom_song/download/10008) — a swipe run of note_slide into note_slide_hold,
|
||||||
// which is the shape that used to arrive as undifferentiated taps. Note the editor writes a
|
// which is the shape that used to arrive as undifferentiated taps. Note the editor writes a
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ static ASSET_VERSIONS: &[AssetVersion] = &[
|
|||||||
AssetVersion { region: "JP", platform: "iOS", version: "7c74b0df372a460d82378e55dc456260", hash: "b77459c89ec94acc4fddc787ded62b95", latest: false },
|
AssetVersion { region: "JP", platform: "iOS", version: "7c74b0df372a460d82378e55dc456260", hash: "b77459c89ec94acc4fddc787ded62b95", latest: false },
|
||||||
|
|
||||||
// Re-written client versions 2.7.0
|
// Re-written client versions 2.7.0
|
||||||
AssetVersion { region: "JP", platform: "Windows", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "94381f2f6bb122b58136077c294b7b11", latest: true },
|
AssetVersion { region: "JP", platform: "Windows", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "c4b96d2ea6e55bd2d2a807c1ac9a1165", latest: true },
|
||||||
AssetVersion { region: "JP", platform: "Android", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "929f1efd0e2164e9740d46873e63e9ba", latest: true },
|
AssetVersion { region: "JP", platform: "Android", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "7cd5fdb3a60df2ad81be19fe5126c92b", latest: true },
|
||||||
AssetVersion { region: "JP", platform: "iOS", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "0bf32a63167e8770a3a0306b10b765e0", latest: true },
|
AssetVersion { region: "JP", platform: "iOS", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "20e62977364b1e8e91040fc024086a56", latest: true },
|
||||||
|
|
||||||
|
|
||||||
AssetVersion { region: "JP", platform: "Linux", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "", latest: true },
|
AssetVersion { region: "JP", platform: "Linux", version: "a984f9b4c8b64f3e81f0bf11cb9c26fb", hash: "", latest: true },
|
||||||
|
|||||||
Reference in New Issue
Block a user