Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SuperNova 2 Support #86

Open
wants to merge 1 commit into
base: trunk
Choose a base branch
from

Conversation

DiscoStarslayer
Copy link

I'm not the best pythoner in the world so some of this code might be cringe, however it is working properly for the following:

Features:

  • e-Amuse card registration and login
  • Last song selection storage
  • Options storage
  • Score storage
  • Calorie storage
  • Events

I still haven't done the following work:

Todo:

  • Expose functionality to frontend UI config (calorie display, events)
  • Identify why calorie values are not loading
  • Identify why last song not storing correctly for sn2 category
  • Doubles mode and course mode logic
  • Traffic gen implementation
  • Unit Tests

Shoutout to @987123879113 for sharing their research with me on the account binary format and the event logic, was a big help in making this work faster.

I plan on getting this a bit more cleaned up and upstream-able eventually, tackling the todos over time, however the branch is fully functional so it would be a shame to keep it to myself till then.

Open to any and all suggestions on the lack of quality on my python code, there was some fishy stuff with the inheritance that I'm not a big fan of but unsure what fits best with the project's vision.

Features:
- e-Amuse card registration and login
- Last song selection storage
- Options storage
- Score storage
- Calorie storage
- Events

ToDo:
- Expose functionality to frontend UI config (calorie display, events)
- Identify why calorie values are not loading
- Identify why last song not storing correctly for sn2 category
- Doubles mode and course mode logic
@@ -127,6 +127,13 @@ def format_profile(self, userid: UserID, profile: Profile) -> Node:
"""
return Node.void("game")

def format_profile_part(self, userid: UserID, profile: Profile, part: str) -> Node:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile for SN2 is split into 3 binary chunks, where to break this logic down, it being here feels wrong

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly the format_profile and unformat_profile convention were both established to just give some sort of unity across games. They don't need to exist outside of the fact that the base of most games assumes it can call that for profile loads. If there's 3 parts here, this can diverge for cleaner code.


return game

def handle_game_hiscore_request(self, request: Node) -> Node:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this stuff is most likely unused, I used the ddrx code as a baseline, need to take a look

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I would check whether these are even in the game at all, I would doubt it.


return root

def handle_info_message_request(self, request: Node) -> Node:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure where this API is used, figure it might get displayed to the user somehow on login but I couldn't work it out

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a string-encoded server message for setting flags? Dunno, that's how bishi does things somewhat.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the game logic seems to store the returned value to memory, but I haven't dug much further than that, I'll take a peek at the Bishi code, that might give me a few clues.


return root

def handle_info_tenpo_request(self, request: Node) -> Node:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks to be some kind of remote-update functionality

idx = 0

# Empty option set is a non-zero opt array, unsure how to do this check with validated_dict cleanly?
if not profile.has_key("opt"):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to add new property to validated_dict to support this, maybe a cleaner way?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A ValidatedDict is just a subclass of a python dict, so you can just do if "opt" not in profile or similar. All of the functions on it are for access or insertion of storage where you want to constrain the type. Under the hood, it's just a Dict[str, object]

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Yes that is much cleaner and much more pythonic, I was looking for a class function but wasn't aware of the in syntax. 🙇

from bemani.common import VersionConstants


class DDRX(DDRBase):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to extract this to fix a circular dependency? Maybe not needed?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDGI, where's the circular dependency? Does SN2 reference X for some reason? This isn't a problem anywhere else.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might try my hand at it again because I think it might have had something to do with how I was importing modules instead of a file directly? But it was mainly a problem with the previous_version function.

How the code used to be:
In Stub: DDRX, DDRSN1
Own Module: DDRSN2

DDRX has a previous_version that points to DDRSN2 from the module, but DDRSN2 has a previous_version that points to DDRSN1 from the stub file. So the stub file needs the DDRSN2 import to work, but that import can't work until the stub file resolves for DDRSN1? That's how I understood it but my imports might have been strange too, so let me review this one a bit more.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, it's because SN1 and X were in the stub file, so circular dependency. You moved the DDRX bits out of the stub into their own file, so it should work now.

@@ -105,8 +114,37 @@ def __get_musicid(self, game: GameConstants, version: int, songid: int, songchar
},
)
if cursor.rowcount != 1:
# music doesn't exist
raise Exception(f"Song {songid} chart {songchart} doesn't exist for game {game} version {version}")
if game == GameConstants.DDR and version == VersionConstants.DDR_SUPERNOVA_2:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure of a clean way to extract the song list from the game yet, so making them on demand here. There is probably a way to do it but the files on the hdd are encrypted so need to think of a method of extracting them.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll need to figure that out, or provide a song DB .tsv or something, because on-demand music stuff isn't supposed to be here. That's why this module has no way of getting the next ID. From it's perspective, the music DB is read-only.

I assume this also means no front-end? I think you mentioned that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah currently no front-end, but it's something I want to add so that the events can be managed among other things.

I wasn't aware of the .tsv files, this might be the approach I'll have to take here, there isn't any public tools for decrypting the dnas blobs that I know of and the easiest way is most likely to extract the data from memory once loaded.

@DragonMinded
Copy link
Owner

Hey, thanks for sending this in, this looks promising! Sorry about the slow response, I'm very burned out. I won't be able to merge this without at minimum traffic tests, and unit tests would be a nice bonus (but most non-core stuff doesn't have any since unit testing game responses tends to end up being a change inhibitor). Let me comment inline with your comments.

@@ -142,6 +149,24 @@ def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -
"""
return oldprofile

def get_profile_by_refid_and_part(self, refid: Optional[str], part: Optional[str]) -> Optional[Node]:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this definitely feels weird. But, the pattern is established. Worth a think.

@@ -0,0 +1,3 @@
from bemani.backend.ddr.ddrsn2.ddrsn2 import DDRSuperNova2
Copy link
Owner

@DragonMinded DragonMinded Feb 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting separating this into a module instead of a single file. Any reason for having this across a bunch of files? Don't see a problem with it, just curious.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, mostly just a leftover from my JS work. A common lint is only one class defined per file. If it's a bit awkward I'm ok with merging them into one file.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IDGAF either way, pattern in the repo is one file, we don't do silly things like have an import system that depends on there being one export/class per file, so you can get away with a lot of stuff.

@DiscoStarslayer
Copy link
Author

Hey, thanks for sending this in, this looks promising! Sorry about the slow response, I'm very burned out.

Hey! Thank you very much for taking the time to review! I honestly didn't think I would get a response so quickly, so thanks a lot for that.

And understood with the requirements for merge, I agree with that and will work towards polishing this branch up. Game is already 17 years old so there is no urgency to any of this 😄

Going over your review now 👀

@AlejandroGuzmanCL
Copy link

Hello, sry for the bump but I was wondering if we can test this somehow, I havent found any docs or guide how to setup this locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants