mirror of
https://git.ethanthesleepy.one/ethanaobrien/ew
synced 2026-08-26 23:12:20 +08:00
Fix a thing
This commit is contained in:
@@ -770,7 +770,6 @@ async fn upload(req: HttpRequest, payload: Multipart) -> HttpResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("UPLOAD5");
|
|
||||||
match create_song(uid, &fields) {
|
match create_song(uid, &fields) {
|
||||||
Ok(music_id) => send_json(object!{
|
Ok(music_id) => send_json(object!{
|
||||||
result: "OK",
|
result: "OK",
|
||||||
|
|||||||
@@ -43,10 +43,22 @@ use jzon::{object, JsonValue};
|
|||||||
// - effect 0 (random) and anything else unknown -> plain type 1. Every effect the game
|
// - effect 0 (random) and anything else unknown -> plain type 1. Every effect the game
|
||||||
// actually defines is covered above, so this is only a floor for hand-authored charts.
|
// actually defines is covered above, so this is only a floor for hand-authored charts.
|
||||||
// - 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; force_sync_group_id stays 0.
|
// 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
|
||||||
// header occupies 100, real groups count up from 101, and notes that hit
|
// header occupies 100 and real groups count up from 101. The client spawns markers one
|
||||||
// simultaneously (equal timing_sec, which covers SIF1 effect 2 pairs) share one num.
|
// num-group at a time, and LiveMarkerControl.CreateMarkerUI (list overload) plain-RETURNS
|
||||||
|
// when the group holds more than 2 markers — so a num may be shared by AT MOST two notes,
|
||||||
|
// or the whole group's head markers never render (hold bands are created by the separate,
|
||||||
|
// unguarded CreateLongMarkerBandUI call, which is why 4 simultaneous holds showed trails
|
||||||
|
// with no heads). Simultaneous notes (equal final time, which covers SIF1 effect 2 pairs
|
||||||
|
// AND synthesized hold tails) are therefore sorted by lane and chunked into pairs: each
|
||||||
|
// chunk gets its own num, and every chunk after the first carries the PREVIOUS chunk's num
|
||||||
|
// in force_sync_group_id. That is exactly the official encoding — all 4292 shipped NoteData
|
||||||
|
// assets have num groups of only 1 or 2, and the 15 charts with 3-4 simultaneous notes
|
||||||
|
// (SIFAC ports, e.g. 1132_5_Sn, 1136_5_An) pair the lowest lanes under the first num and
|
||||||
|
// point the later chunk's m_ForceSyncGroupID at it. The client turns that into the extra
|
||||||
|
// connector line (LiveTimeController.CreateMarkerTimeData force-group pass matches
|
||||||
|
// ForceGroupId against the other chunk's GroupId and links the lane-closest pair).
|
||||||
// - notes[0] is ALWAYS the dummy header (id 0, num 100, type 0) - the client
|
// - notes[0] is ALWAYS the dummy header (id 0, num 100, type 0) - the client
|
||||||
// deserializes it verbatim.
|
// deserializes it verbatim.
|
||||||
// - max_combo_count = all real notes EXCEPT hold heads whose tail is on the same
|
// - max_combo_count = all real notes EXCEPT hold heads whose tail is on the same
|
||||||
@@ -183,16 +195,33 @@ pub fn transcode(beatmap: &JsonValue) -> Result<(JsonValue, i64), String> {
|
|||||||
|
|
||||||
let mut ids = vec![0i64; work.len()];
|
let mut ids = vec![0i64; work.len()];
|
||||||
let mut nums = vec![0i64; work.len()];
|
let mut nums = vec![0i64; work.len()];
|
||||||
|
let mut force_sync = vec![0i64; work.len()];
|
||||||
let mut num = 100;
|
let mut num = 100;
|
||||||
let mut last_time = f64::NEG_INFINITY;
|
|
||||||
for (i, index) in order.iter().enumerate() {
|
for (i, index) in order.iter().enumerate() {
|
||||||
ids[*index] = (i + 1) as i64;
|
ids[*index] = (i + 1) as i64;
|
||||||
// Simultaneous notes share a spawn group
|
|
||||||
if work[*index].time != last_time {
|
|
||||||
num += 1;
|
|
||||||
last_time = work[*index].time;
|
|
||||||
}
|
}
|
||||||
|
// Spawn groups: at most TWO notes per num (see the header comment — a bigger group's head
|
||||||
|
// markers never render). A cluster of simultaneous notes is sorted by lane and chunked into
|
||||||
|
// pairs; the leftmost pair takes the first num, and each later chunk points its
|
||||||
|
// force_sync_group_id at the previous chunk's num, matching the official SIFAC-port encoding.
|
||||||
|
let mut start = 0;
|
||||||
|
while start < order.len() {
|
||||||
|
let mut end = start + 1;
|
||||||
|
while end < order.len() && work[order[end]].time == work[order[start]].time {
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
let mut cluster: Vec<usize> = order[start..end].to_vec();
|
||||||
|
cluster.sort_by_key(|index| work[*index].line);
|
||||||
|
let mut prev_num = 0;
|
||||||
|
for chunk in cluster.chunks(2) {
|
||||||
|
num += 1;
|
||||||
|
for index in chunk {
|
||||||
nums[*index] = num;
|
nums[*index] = num;
|
||||||
|
force_sync[*index] = prev_num;
|
||||||
|
}
|
||||||
|
prev_num = num;
|
||||||
|
}
|
||||||
|
start = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut notes = jzon::array![{
|
let mut notes = jzon::array![{
|
||||||
@@ -221,7 +250,7 @@ pub fn transcode(beatmap: &JsonValue) -> Result<(JsonValue, i64), String> {
|
|||||||
"child_id": if let Some(child) = note.child { ids[child] } else { 0 },
|
"child_id": if let Some(child) = note.child { ids[child] } else { 0 },
|
||||||
"child_num": if let Some(child) = note.child { nums[child] } else { 0 },
|
"child_num": if let Some(child) = note.child { nums[child] } else { 0 },
|
||||||
"child_line": if let Some(child) = note.child { work[child].line } else { 0 },
|
"child_line": if let Some(child) = note.child { work[child].line } else { 0 },
|
||||||
"force_sync_group_id": 0
|
"force_sync_group_id": force_sync[*index]
|
||||||
}).unwrap();
|
}).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,6 +353,22 @@ mod tests {
|
|||||||
assert_eq!(tail["time"].as_f64().unwrap(), 3.5);
|
assert_eq!(tail["time"].as_f64().unwrap(), 3.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The client spawns markers one num-group at a time and CreateMarkerUI refuses lists of
|
||||||
|
// more than 2, so no num may ever be shared by 3+ notes (official charts never do)
|
||||||
|
fn assert_spawn_groups_hold_at_most_two(chart: &JsonValue) {
|
||||||
|
let mut counts: Vec<(i64, i64)> = Vec::new();
|
||||||
|
for data in chart["notes"].members().skip(1) {
|
||||||
|
let num = data["num"].as_i64().unwrap();
|
||||||
|
match counts.iter_mut().find(|(n, _)| *n == num) {
|
||||||
|
Some((_, c)) => *c += 1,
|
||||||
|
None => counts.push((num, 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (num, count) in counts {
|
||||||
|
assert!(count <= 2, "num {} is shared by {} notes; the client renders no heads for such a group", num, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parallel_pair() {
|
fn parallel_pair() {
|
||||||
let beatmap = jzon::array![
|
let beatmap = jzon::array![
|
||||||
@@ -337,6 +382,83 @@ mod tests {
|
|||||||
assert_eq!(chart["notes"][1]["num"], chart["notes"][2]["num"].clone());
|
assert_eq!(chart["notes"][1]["num"], chart["notes"][2]["num"].clone());
|
||||||
assert_eq!(chart["notes"][1]["type"], 1);
|
assert_eq!(chart["notes"][1]["type"], 1);
|
||||||
assert_eq!(chart["notes"][2]["type"], 1);
|
assert_eq!(chart["notes"][2]["type"], 1);
|
||||||
|
// A plain pair is the GroupSync path; the force-group field stays clear
|
||||||
|
assert_eq!(chart["notes"][1]["force_sync_group_id"], 0);
|
||||||
|
assert_eq!(chart["notes"][2]["force_sync_group_id"], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn three_simultaneous_notes_split_into_pair_plus_force_synced_single() {
|
||||||
|
// Official encoding (e.g. 1132_5_Sn, 1136_5_An: the only shipped charts with 3-4
|
||||||
|
// simultaneous notes): the cluster is sorted by lane, the lowest two lanes share the
|
||||||
|
// first num, and the leftover note takes the NEXT num with force_sync_group_id pointing
|
||||||
|
// back at the pair's num. Input arrives lane-scrambled to prove the chunking sorts.
|
||||||
|
let beatmap = jzon::array![
|
||||||
|
sif_note(1.0, 8, 1, 0.0),
|
||||||
|
sif_note(1.0, 2, 1, 0.0),
|
||||||
|
sif_note(1.0, 5, 1, 0.0)
|
||||||
|
];
|
||||||
|
let (chart, combo) = transcode(&beatmap).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(combo, 3);
|
||||||
|
assert_eq!(chart["notes"].len(), 4);
|
||||||
|
assert_spawn_groups_hold_at_most_two(&chart);
|
||||||
|
// Emission keeps input order on time ties; grouping is by lane
|
||||||
|
let (right, left, mid) = (&chart["notes"][1], &chart["notes"][2], &chart["notes"][3]);
|
||||||
|
assert_eq!(left["line"], 1);
|
||||||
|
assert_eq!(mid["line"], 4);
|
||||||
|
assert_eq!(right["line"], 7);
|
||||||
|
// Lanes 1 and 4 pair under the first num, force-clear
|
||||||
|
assert_eq!(left["num"], 101);
|
||||||
|
assert_eq!(mid["num"], 101);
|
||||||
|
assert_eq!(left["force_sync_group_id"], 0);
|
||||||
|
assert_eq!(mid["force_sync_group_id"], 0);
|
||||||
|
// Lane 7 rides the next num and force-syncs against the pair's num
|
||||||
|
assert_eq!(right["num"], 102);
|
||||||
|
assert_eq!(right["force_sync_group_id"], 101);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn four_simultaneous_holds_pair_heads_and_tails() {
|
||||||
|
// Second field report: 4 holds hitting together rendered their bands but no head
|
||||||
|
// markers — all four heads shared one num, and the client's CreateMarkerUI refuses
|
||||||
|
// groups over 2 while CreateLongMarkerBandUI (a separate, unguarded call) still drew
|
||||||
|
// the bands. Officially both the head cluster AND the tail cluster split 2+2 with the
|
||||||
|
// second chunk force-synced to the first (1132_5_Sn time 12.208 does this to tails).
|
||||||
|
let beatmap = jzon::array![
|
||||||
|
sif_note(1.0, 3, 3, 2.0),
|
||||||
|
sif_note(1.0, 4, 3, 2.0),
|
||||||
|
sif_note(1.0, 6, 3, 2.0),
|
||||||
|
sif_note(1.0, 7, 3, 2.0)
|
||||||
|
];
|
||||||
|
let (chart, combo) = transcode(&beatmap).unwrap();
|
||||||
|
|
||||||
|
// Same-lane hold heads don't count; the four tails do
|
||||||
|
assert_eq!(combo, 4);
|
||||||
|
assert_eq!(chart["notes"].len(), 9);
|
||||||
|
assert_spawn_groups_hold_at_most_two(&chart);
|
||||||
|
|
||||||
|
// Heads at 1.0: lanes 2,3 share num 101; lanes 5,6 share num 102 force-synced to 101
|
||||||
|
for (index, line, num, fs) in [(1, 2, 101, 0), (2, 3, 101, 0), (3, 5, 102, 101), (4, 6, 102, 101)] {
|
||||||
|
let head = &chart["notes"][index];
|
||||||
|
assert_eq!(head["line"], line, "head {}", index);
|
||||||
|
assert_eq!(head["num"], num, "head {}", index);
|
||||||
|
assert_eq!(head["force_sync_group_id"], fs, "head {}", index);
|
||||||
|
assert_eq!(head["parent_id"], 0, "head {}", index);
|
||||||
|
}
|
||||||
|
// Tails at 3.0: the SAME pairing applies to the synthesized cluster
|
||||||
|
for (index, line, num, fs) in [(5, 2, 103, 0), (6, 3, 103, 0), (7, 5, 104, 103), (8, 6, 104, 103)] {
|
||||||
|
let tail = &chart["notes"][index];
|
||||||
|
assert_eq!(tail["line"], line, "tail {}", index);
|
||||||
|
assert_eq!(tail["num"], num, "tail {}", index);
|
||||||
|
assert_eq!(tail["force_sync_group_id"], fs, "tail {}", index);
|
||||||
|
assert_eq!(tail["child_id"], 0, "tail {}", index);
|
||||||
|
}
|
||||||
|
// The chains still line up: each head's child_num names the tail's spawn group
|
||||||
|
assert_eq!(chart["notes"][1]["child_id"], 5);
|
||||||
|
assert_eq!(chart["notes"][1]["child_num"], 103);
|
||||||
|
assert_eq!(chart["notes"][3]["child_id"], 7);
|
||||||
|
assert_eq!(chart["notes"][3]["child_num"], 104);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user