llllllllll / slider

Utilities for working with osu! files and data
https://llllllllll.github.io/slider/index.html
GNU Lesser General Public License v3.0
39 stars 17 forks source link

enables Beatmaps to be written back to .osu files #101

Closed IMXZ1234 closed 2 years ago

IMXZ1234 commented 2 years ago

pack function is added for HitObject and Curve as well. Written .osu has been tested and can be recognized and played by osu! client. Colours section in .osu file is omitted since slider does not parse this section. Support for parsing colours may be added in the future, so the generated .osu can include this section. Default values are chosen to be the same as those used by the Beatmap editor which is embeded in osu! client.

IMXZ1234 commented 2 years ago

However, during the test of the generated .osu file, all HitObjects are interpreted as in a single combo. I think the problem is with the parsing phase. When parsing HitObjects, combo information which is saved together in one int with hit object type_code is dropped. This may be fixed, perhaps, by adding extra is_new_combo and combo_colour member to HitObject to record the combo information.

tybug commented 2 years ago

Before I take a look at this, can you fix the inspection errors? (you might want to lint locally with flake8 instead of pushing to test; from experience, fixing one inspection often causes another to pop up). Github made me manually approve the workflows to run on this pr since you're a first time contributor, which is why it didn't run when you first opened the PR.

IMXZ1234 commented 2 years ago

Inspection errors are all E501 line too long, now fixed. Also included a few minor changes:

  1. Shorten the function name _replace_invalid_with_default to _invalid_to_default.
  2. Prevent Bookmarks field in Editor section from appearing in generated .osu file if it is an empty list.
IMXZ1234 commented 2 years ago

I believe the problem in my former review boils down to this: Using default test beatmap AKINO from bless4 & CHiCO with HoneyWorks - MIIRO vs. Ai no Scenario (monstrata) [Tatoe].osu, Simply add a test in slider/tests/test_beatmap.py like this:

def test_parse(beatmap):
    print(beatmap.hit_objects()[12])

the result will be:

<Circle: Position(x=52.647999999999996, y=201.648), 4923ms>

While in the original .osu file the line recording this hit object is:

49,198,4923,5,0,2:0:0:0:

Position is incorrect.

IMXZ1234 commented 2 years ago

Now I learn the problem can be solved by setting the stacking parameter of Beatmap.hit_objects() to False to disable the _resolve_stacking function turned on by default. Maybe changing its default to False is better?

tybug commented 2 years ago

I don't think stacking=False would be a good default. Even ignoring the fact that changing it would be a breaking change, most uses of slider want the position of the hitobject as it would appear in gameplay, which is what stacking provides.

IMXZ1234 commented 2 years ago

I had written tests.test_beatmap.test_pack() as a draft script rather than a formal piece of code. To avoid writing long segments of attribute names I had turned to __dict__. Well, the code just grew grotesque. It was my fault to have valued convenience over readability. Now it has been refined, and I think it's neater now. Since pack() is a member method, I suppose directly accessing the private member _hit_objects is all right. Also, it promotes efficiency since self.hit_objects(stacking=False) does quite a number of checks inside and actually constructs a new tuple on its return. Thus I suppose it would be better to use _hit_objects instead.

tybug commented 2 years ago

please don't force push if you can absolutely avoid it. It messes up the history of anyone who has checked out this pr (ie, me).

tybug commented 2 years ago

Since pack() is a member method, I suppose directly accessing the private member _hit_objects is all right. Also, it promotes efficiency since self.hit_objects(stacking=False) does quite a number of checks inside and actually constructs a new tuple on its return. Thus I suppose it would be better to use _hit_objects instead.

generally I prefer accessing known apis (ie self.hit_objects()) where possible, even inside member methods. But on second thought this is actually probably better since you really do want the "raw" representation of the hitobjects, since you're about to write them back to a .osu file.

tybug commented 2 years ago

I've cleaned up the testing code, slightly opinionated-ly. The tuple(map( business in was particularly bad imo, and written more cleanly with a zip. The only behavioral changes I made were to access beatmap._hit_objects instead of beatmap.hit_objects(stacking=False) (you convinced me this was the better path forward above), and to assert something slightly stronger with the timing points:

assert (tp1.parent is not None) == (tp2.parent is not None)

instead of

        if timing_point1.parent is None:
            assert timing_point2.parent is None

which only catches the case of "tp1 parent none and tp2 parent not none", and not "tp1 parent not none and tp2 parent none".

Let me know if you hugely disagree with any of my changes. I'll do another pass on this pr soon, but the testing code looks good to me now.

IMXZ1234 commented 2 years ago

which only catches the case of "tp1 parent none and tp2 parent not none", and not "tp1 parent not none and tp2 parent none".

Good insight. I have no further problem.

tybug commented 2 years ago

A few things:

The largest remaining thing to do is combo colors, which as you've noted above we currently discard. I think a good path forward would be changing how we store the type_code for hitobjects by keeping the entire value of type_code from the parsed hitobject instead of just what hitobject type it is. I'll leave that for another pull, if at all.

I think this PR is good to go now. I'll wait to make sure you don't have any problems with my recent changes, and then merge.

IMXZ1234 commented 2 years ago

I think it may be better to replace the nested function _pack_bookmarks with function _pack_timedelta_list which is generally available.

IMXZ1234 commented 2 years ago

Custom separators can now be used, by passing partial(_pack_xx_list, sep=xx) to _pack_field. the field name, field value, and default are shared parameters among all _pack_xx functions but separator is only needed in _pack_xx_list functions. Making these three argument mandatory while keeping separator as optional sounds sensible to me. Another solution maybe using **kwargs, but I do not use that since usage of optional parameters is not common(only _pack_xx_list need optional parameters). Nonetheless, if you still find such a solution absurd, feel free to discard this change. Other than these, I think everything is well.

tybug commented 2 years ago

still feels like an overengineered solution (re: separators), but I'm not going to fuss too much. That may also just be my bias against this functional style of programming showing. Let's get this in as-is. Thanks for the pr!