# vim: set fileencoding=utf-8
import random
from typing import Any, Dict, List, Optional, Set, Tuple
from typing_extensions import Final

from bemani.backend.jubeat.base import JubeatBase
from bemani.backend.jubeat.common import (
    JubeatDemodataGetHitchartHandler,
    JubeatDemodataGetNewsHandler,
    JubeatGamendRegisterHandler,
    JubeatGametopGetMeetingHandler,
    JubeatLobbyCheckHandler,
    JubeatLoggerReportHandler,
)
from bemani.backend.jubeat.clan import JubeatClan

from bemani.backend.base import Status
from bemani.common import Profile, Time, ValidatedDict, VersionConstants
from bemani.data import Data, UserID, Score, Song
from bemani.protocol import Node


class JubeatFesto(
    JubeatDemodataGetHitchartHandler,
    JubeatDemodataGetNewsHandler,
    JubeatGamendRegisterHandler,
    JubeatGametopGetMeetingHandler,
    JubeatLobbyCheckHandler,
    JubeatLoggerReportHandler,
    JubeatBase,
):
    name: str = "Jubeat Festo"
    version: int = VersionConstants.JUBEAT_FESTO

    JBOX_EMBLEM_NORMAL: Final[int] = 1
    JBOX_EMBLEM_PREMIUM: Final[int] = 2

    EVENT_STATUS_OPEN: Final[int] = 0x1
    EVENT_STATUS_COMPLETE: Final[int] = 0x2

    EVENTS: Dict[int, Dict[str, bool]] = {
        5: {
            "enabled": False,
        },
        6: {
            "enabled": False,
        },
        # Something to do with maintenance mode?
        15: {
            "enabled": True,
        },
        22: {
            "enabled": False,
        },
        23: {
            "enabled": False,
        },
        33: {
            "enabled": False,
        },
        101: {
            "enabled": False,
        },
        102: {
            "enabled": False,
        },
        103: {
            "enabled": False,
        },
        104: {
            "enabled": False,
        },
        105: {
            "enabled": False,
        },
        106: {
            "enabled": False,
        },
        107: {
            "enabled": False,
        },
        108: {
            "enabled": False,
        },
        109: {
            "enabled": False,
        },
    }

    COURSE_STATUS_SEEN: Final[int] = 0x01
    COURSE_STATUS_PLAYED: Final[int] = 0x02
    COURSE_STATUS_CLEARED: Final[int] = 0x04

    COURSE_TYPE_PERMANENT: Final[int] = 1
    COURSE_TYPE_TIME_BASED: Final[int] = 2

    COURSE_CLEAR_SCORE: Final[int] = 1
    COURSE_CLEAR_COMBINED_SCORE: Final[int] = 2
    COURSE_CLEAR_HAZARD: Final[int] = 3

    COURSE_HAZARD_EXC1: Final[int] = 1
    COURSE_HAZARD_EXC2: Final[int] = 2
    COURSE_HAZARD_EXC3: Final[int] = 3
    COURSE_HAZARD_FC1: Final[int] = 4
    COURSE_HAZARD_FC2: Final[int] = 5
    COURSE_HAZARD_FC3: Final[int] = 6

    GAME_CHART_TYPE_BASIC: Final[int] = 0
    GAME_CHART_TYPE_ADVANCED: Final[int] = 1
    GAME_CHART_TYPE_EXTREME: Final[int] = 2

    # Return the netlog service so that Festo doesn't crash on coin-in.
    extra_services: List[str] = [
        "netlog",
        "slocal",
    ]

    def previous_version(self) -> Optional[JubeatBase]:
        return JubeatClan(self.data, self.config, self.model)

    def game_to_db_chart(self, game_chart: int, hard_mode: bool) -> int:
        if hard_mode:
            return {
                self.GAME_CHART_TYPE_BASIC: self.CHART_TYPE_HARD_BASIC,
                self.GAME_CHART_TYPE_ADVANCED: self.CHART_TYPE_HARD_ADVANCED,
                self.GAME_CHART_TYPE_EXTREME: self.CHART_TYPE_HARD_EXTREME,
            }[game_chart]
        else:
            return {
                self.GAME_CHART_TYPE_BASIC: self.CHART_TYPE_BASIC,
                self.GAME_CHART_TYPE_ADVANCED: self.CHART_TYPE_ADVANCED,
                self.GAME_CHART_TYPE_EXTREME: self.CHART_TYPE_EXTREME,
            }[game_chart]

    def db_to_game_chart(self, db_chart: int) -> int:
        return {
            self.CHART_TYPE_BASIC: self.GAME_CHART_TYPE_BASIC,
            self.CHART_TYPE_ADVANCED: self.GAME_CHART_TYPE_ADVANCED,
            self.CHART_TYPE_EXTREME: self.GAME_CHART_TYPE_EXTREME,
            self.CHART_TYPE_HARD_BASIC: self.GAME_CHART_TYPE_BASIC,
            self.CHART_TYPE_HARD_ADVANCED: self.GAME_CHART_TYPE_ADVANCED,
            self.CHART_TYPE_HARD_EXTREME: self.GAME_CHART_TYPE_EXTREME,
        }[db_chart]

    @classmethod
    def run_scheduled_work(cls, data: Data, config: Dict[str, Any]) -> List[Tuple[str, Dict[str, Any]]]:
        """
        Insert daily FC challenges into the DB.
        """
        events: List[Tuple[str, Dict[str, Any]]] = []
        if data.local.network.should_schedule(cls.game, cls.version, "fc_challenge", "daily"):
            # Generate a new list of two FC challenge songs.
            start_time, end_time = data.local.network.get_schedule_duration("daily")
            all_songs = list(set(song.id for song in data.local.music.get_all_songs(cls.game, cls.version)))
            if len(all_songs) >= 2:
                daily_songs = random.sample(all_songs, 2)
                data.local.game.put_time_sensitive_settings(
                    cls.game,
                    cls.version,
                    "fc_challenge",
                    {
                        "start_time": start_time,
                        "end_time": end_time,
                        "today": daily_songs[0],
                        "whim": daily_songs[1],
                    },
                )
                events.append(
                    (
                        "jubeat_fc_challenge_charts",
                        {
                            "version": cls.version,
                            "today": daily_songs[0],
                            "whim": daily_songs[1],
                        },
                    )
                )

                # Mark that we did some actual work here.
                data.local.network.mark_scheduled(cls.game, cls.version, "fc_challenge", "daily")
        if data.local.network.should_schedule(cls.game, cls.version, "random_course", "daily"):
            # Generate a new list of three random songs for random course mode.
            start_time, end_time = data.local.network.get_schedule_duration("daily")

            def is_ten(song: Song) -> bool:
                # We only want random songs that actually have a hard chart! This should be all songs right now,
                # but let's be conservative in case a future game screws things up.
                if song.chart not in {
                    cls.CHART_TYPE_HARD_BASIC,
                    cls.CHART_TYPE_HARD_ADVANCED,
                    cls.CHART_TYPE_HARD_EXTREME,
                }:
                    return False

                difficulty = song.data.get_float("difficulty", 13)
                if difficulty == 13.0:
                    difficulty = float(song.data.get_int("difficulty", 13))

                return difficulty >= 10.0 and difficulty < 11.0

            def chart_lut(chart: int) -> int:
                return {
                    cls.CHART_TYPE_HARD_BASIC: cls.CHART_TYPE_BASIC,
                    cls.CHART_TYPE_HARD_ADVANCED: cls.CHART_TYPE_ADVANCED,
                    cls.CHART_TYPE_HARD_EXTREME: cls.CHART_TYPE_EXTREME,
                }[chart]

            all_tens = [song for song in data.local.music.get_all_songs(cls.game, cls.version) if is_ten(song)]
            if len(all_tens) >= 3:
                course_songs = random.sample(all_tens, 3)
                data.local.game.put_time_sensitive_settings(
                    cls.game,
                    cls.version,
                    "random_course",
                    {
                        "start_time": start_time,
                        "end_time": end_time,
                        "song1": {
                            "id": course_songs[0].id,
                            "chart": chart_lut(course_songs[0].chart),
                        },
                        "song2": {
                            "id": course_songs[1].id,
                            "chart": chart_lut(course_songs[1].chart),
                        },
                        "song3": {
                            "id": course_songs[2].id,
                            "chart": chart_lut(course_songs[2].chart),
                        },
                    },
                )
                events.append(
                    (
                        "jubeat_random_course_charts",
                        {
                            "version": cls.version,
                            "song1": {
                                "id": course_songs[0].id,
                                "chart": chart_lut(course_songs[0].chart),
                            },
                            "song2": {
                                "id": course_songs[1].id,
                                "chart": chart_lut(course_songs[1].chart),
                            },
                            "song3": {
                                "id": course_songs[2].id,
                                "chart": chart_lut(course_songs[2].chart),
                            },
                        },
                    )
                )

                # Mark that we did some actual work here.
                data.local.network.mark_scheduled(cls.game, cls.version, "random_course", "daily")
        return events

    @classmethod
    def get_settings(cls) -> Dict[str, Any]:
        """
        Return all of our front-end modifiably settings.
        """
        return {
            "ints": [
                {
                    "name": "KAC Course Phase",
                    "tip": "The KAC competition courses that should be available in Tune Run",
                    "category": "game_config",
                    "setting": "kac_phase",
                    "values": {
                        0: "No KAC phase",
                        1: "The 8th KAC",
                        2: "The 9th KAC",
                        3: "The 10th KAC",
                    },
                },
            ],
            "bools": [
                {
                    "name": "Enable Stone Tablet Event",
                    "tip": "Enables the Stone Tablet event",
                    "category": "game_config",
                    "setting": "festo_dungeon",
                },
                {
                    "name": "50th Anniversary Celebration",
                    "tip": "Display the 50th anniversary screen in attract mode",
                    "category": "game_config",
                    "setting": "50th_anniversary",
                },
                {
                    "name": "Force Unlock All Songs",
                    "tip": "Forces all songs to be available by default",
                    "category": "game_config",
                    "setting": "force_song_unlock",
                },
            ],
        }

    def __get_course_list(self) -> List[Dict[str, Any]]:
        # Note that several of the below courses originally included removed songs, because older
        # versions of the game included them and they were removed sometime in its 4 year lifespan.
        # For those courses, the game will not display them unless you are on a data version that
        # includes the song. BemaniWiki notes when they were changed, so we check to see what the
        # data version of the game running is to provide the correct course. We could just support
        # only final data, but that's no fun!
        dataver = self.model.version or 2022052400

        # If it is available, then grab the random course. If we haven't generated that course, then
        # just don't bother trying to create it.
        entry = self.data.local.game.get_time_sensitive_settings(self.game, self.version, "random_course")
        random_course: List[Dict[str, Any]] = []

        if entry is not None:
            song1 = entry.get_dict("song1")
            song2 = entry.get_dict("song2")
            song3 = entry.get_dict("song3")

            random_course = [
                {
                    "id": 57,
                    "name": "腕試し！ランダムコース",
                    "course_type": self.COURSE_TYPE_PERMANENT,
                    "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                    "hard": True,
                    "difficulty": 15,
                    "score": 2850000,
                    "music": [
                        [(song1.get_int("id"), song1.get_int("chart"))],
                        [(song2.get_int("id"), song2.get_int("chart"))],
                        [(song3.get_int("id"), song3.get_int("chart"))],
                    ],
                }
            ]

        # So the game has a hard limit of 60 courses, but if we include everything from the
        # lifetime of festo including the random course we end up with 61 courses, which means
        # the game truncates the last course. Boo, that sucks. So, we have to have certain
        # courses switched. Let's do the KAC courses since in real life there would never have
        # been multiple KAC courses available at once.
        game_config = self.get_game_config()
        kac_phase = game_config.get_int("kac_phase")
        kac_8th = []
        kac_9th = []
        kac_10th = []

        if kac_phase == 1:
            kac_8th = [
                {
                    "id": 41,
                    "name": "The 8th KAC 個人部門",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000052, 2)],
                        [(90000013, 2)],
                        [(70000167, 2)],
                    ],
                },
                {
                    "id": 42,
                    "name": "The 8th KAC 団体部門",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000009, 2)],
                        [(80000133, 2)],
                        [(80000101, 2)],
                    ],
                },
            ]
        elif kac_phase == 2:
            kac_9th = [
                {
                    "id": 201,
                    "name": "The 9th KAC 1st Stage 個人部門",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000125, 2)],
                        [(60000065, 2)],
                        [(90000023, 2)],
                    ],
                },
                {
                    "id": 202,
                    "name": "The 9th KAC 1st Stage 団体部門",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000125, 2)],
                        [(50000135, 2)],
                        [(90000045, 2)],
                    ],
                },
                {
                    "id": 203,
                    "name": "The 9th KAC 2nd Stage 個人部門",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000095, 2)],
                        [(80000085, 2)],
                        [(80000090, 2)],
                    ],
                },
                {
                    "id": 204,
                    "name": "The 9th KAC 2nd Stage 団体部門",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000113, 2)],
                        [(50000344, 2)],
                        [(90000096, 2)],
                    ],
                },
            ]
        elif kac_phase == 3:
            kac_10th = [
                {
                    "id": 205,
                    "name": "The 10th KAC 1st Stage",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000003, 2)],
                        [(90000151, 2)],
                        [(90000174, 2)],
                    ],
                },
                {
                    "id": 206,
                    "name": "The 10th KAC 2nd Stage",
                    "course_type": self.COURSE_TYPE_TIME_BASED,
                    "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                    "clear_type": self.COURSE_CLEAR_SCORE,
                    "hard": True,
                    "difficulty": 14,
                    "score": 700000,
                    "music": [
                        [(90000121, 2)],
                        [(90000113, 2)],
                        [(90000124, 2)],
                    ],
                },
            ]

        return [
            # ASARI CUP
            {
                "id": 1,
                "name": "はじめてのビーチ",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 1,
                "score": 700000,
                "music": [
                    [
                        (60000080, 0),
                        (90000025 if dataver < 2021081600 else 90000077, 0),
                        (90000040 if dataver < 2021081600 else 90000139, 0),
                    ],
                    [(60000086, 0), (70000047, 0)],
                    [(90000027 if dataver < 2021081600 else 90000141, 0)],
                ],
            },
            {
                "id": 2,
                "name": "【初段】超幸せハイテンション",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 1,
                "score": 700000,
                "music": [
                    [
                        (
                            70000057 if dataver < 2019062100 else (90000079 if dataver < 2022021600 else 20000031),
                            0,
                        ),
                        (60000100, 0),
                        (90000030 if dataver < 2021081600 else 90000078, 0),
                    ],
                    [(70000125, 0), (90000050, 0)],
                    [(70000106, 0)],
                ],
            },
            {
                "id": 3,
                "name": "アニメランニング",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 2,
                "score": 750000,
                "music": [
                    [
                        (
                            80000020 if dataver < 2019062100 else (90000082 if dataver < 2022021600 else 60000092),
                            0,
                        ),
                        (90000031, 0),
                        (90000037 if dataver < 2021081600 else 90000172, 0),
                    ],
                    [
                        (
                            (
                                80000034
                                if dataver < 2020062900
                                else (
                                    30000108
                                    if dataver < 2020091300
                                    else (40000107 if dataver < 2021020100 else 30000004)
                                )
                            ),
                            0,
                        ),
                        (80000120 if dataver < 2021020200 else 80000059, 0),
                    ],
                    [
                        (80000125 if dataver < 2021031900 else 50000209, 0),
                    ],
                ],
            },
            {
                "id": 4,
                "name": "パブリックリゾート",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 2,
                "score": 750000,
                "music": [
                    [
                        (
                            70000148 if dataver < 2020021900 else (90000040 if dataver < 2021081600 else 80000097),
                            0,
                        ),
                        (50000296 if dataver < 2021081600 else 90000029, 0),
                        (90000044 if dataver < 2021081600 else 90000076, 0),
                    ],
                    [
                        (90000033 if dataver < 2021081600 else 80000093, 0),
                        (90000039 if dataver < 2021081600 else 90000048, 0),
                    ],
                    [
                        (80000091 if dataver < 2021020200 else 80000038, 0),
                    ],
                ],
            },
            {
                "id": 5,
                "name": "【二段】その笑顔は甘く蕩ける",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 3,
                "score": 800000,
                "music": [
                    [(50000268, 0), (70000039, 0), (90000051, 0)],
                    [(70000091, 0), (70000042, 0)],
                    [(60000053, 0)],
                ],
            },
            {
                "id": 6,
                "name": "#オレのユビティズム",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 3,
                "score": 2100000,
                "music": [
                    [(20000042, 0), (20000042, 1), (20000042, 2)],
                    [(70000119, 0), (70000119, 1), (70000119, 2)],
                    [(50000115, 0), (50000115, 1), (50000115, 2)],
                ],
            },
            # KISAGO CUP
            {
                "id": 11,
                "name": "電脳享受空間",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 800000,
                "music": [
                    [
                        (70000046, 1),
                        (70000160, 1),
                        (80000126 if dataver < 2021020200 else 50000233, 1),
                    ],
                    [(80000031, 1), (80000097, 1)],
                    [(90000049, 1)],
                ],
            },
            {
                "id": 12,
                "name": "孤高の少女は破滅を願う",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 850000,
                "music": [
                    [(50000202, 0), (70000117, 0), (70000134, 0)],
                    [(50000212, 0), (80000124, 1)],
                    [(90001008, 1)],
                ],
            },
            {
                "id": 13,
                "name": "スタミナアップ！",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 5,
                "score": 2600000,
                "music": [
                    [
                        (50000242, 0),
                        (
                            80000034 if dataver < 2020063000 else (90000079 if dataver < 2022021600 else 50000277),
                            1,
                        ),
                        (90000037 if dataver < 2021081600 else 50000294, 1),
                    ],
                    [(50000260, 1), (50000261, 1)],
                    [
                        (
                            70000085 if dataver < 2019062100 else (90000081 if dataver < 2022021600 else 90000143),
                            1,
                        ),
                    ],
                ],
            },
            {
                "id": 14,
                "name": "【三段】この花を貴方へ",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 850000,
                "music": [
                    [
                        (20000111 if dataver < 2019062100 else 90000034, 1),
                        (90000037 if dataver < 2021081600 else 90000107, 1),
                        (
                            70000131 if dataver < 2019111800 else (90000042 if dataver < 2021081600 else 90000140),
                            1,
                        ),
                    ],
                    [
                        (80000120 if dataver < 2021020200 else 80000052, 1),
                        (80001010, 1),
                    ],
                    [(40000051, 1)],
                ],
            },
            {
                "id": 15,
                "name": "【四段】嗚呼、大繁盛！",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 6,
                "score": 2600000,
                "music": [
                    [(50000085, 2), (50000237, 2), (80000080, 2)],
                    [(50000172, 2), (50000235, 2)],
                    [(70000065, 2)],
                ],
            },
            {
                "id": 16,
                "name": "#シャレを言いなシャレ",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 4,
                "score": 2400000,
                "music": [
                    [(70000003, 0), (70000003, 1), (70000003, 2)],
                    [(70000045, 0), (70000045, 1), (70000045, 2)],
                    [(70000076, 0), (70000076, 1), (70000076, 2)],
                ],
            },
            {
                "id": 101,
                "name": "jubeat大回顧展 ROOM 1",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 950000,
                "music": [
                    [(50000277, 0), (50000277, 1), (50000277, 2)],
                    [(50000325, 0), (50000325, 1), (50000325, 2)],
                    [(90000014, 0), (90000014, 1), (90000014, 2)],
                ],
            },
            {
                "id": 102,
                "name": "jubeat大回顧展 ROOM 2",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 4,
                "score": 2750000,
                "music": [
                    [(30000048, 0), (30000048, 1), (30000048, 2)],
                    [(30000121, 0), (30000121, 1), (30000121, 2)],
                    [(90000012, 0), (90000012, 1), (90000012, 2)],
                ],
            },
            {
                "id": 103,
                "name": "jubeat大回顧展 ROOM 3",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 925000,
                "music": [
                    [(60000007, 0), (60000007, 1), (60000007, 2)],
                    [(60000070, 0), (60000070, 1), (60000070, 2)],
                    [(90000016, 0), (90000016, 1), (90000016, 2)],
                ],
            },
            {
                "id": 104,
                "name": "jubeat大回顧展 ROOM 4",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 4,
                "score": 2800000,
                "music": [
                    [(40000051, 0), (40000051, 1), (40000051, 2)],
                    [(40000129, 0), (40000129, 1), (40000129, 2)],
                    [(90000013, 0), (90000013, 1), (90000013, 2)],
                ],
            },
            {
                "id": 105,
                "name": "jubeat大回顧展 ROOM 5",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 4,
                "score": 2775000,
                "music": [
                    [(70000177, 0), (70000177, 1), (70000177, 2)],
                    [(70000011, 0), (70000011, 1), (70000011, 2)],
                    [(90000017, 0), (90000017, 1), (90000017, 2)],
                ],
            },
            {
                "id": 106,
                "name": "jubeat大回顧展 ROOM 6",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 940000,
                "music": [
                    [(20000123, 0), (20000123, 1), (20000123, 2)],
                    [(20000038, 0), (20000038, 1), (20000038, 2)],
                    [(90000011, 0), (90000011, 1), (90000011, 2)],
                ],
            },
            {
                "id": 107,
                "name": "jubeat大回顧展 ROOM 7",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 950000,
                "music": [
                    [(50000021, 0), (50000021, 1), (50000021, 2)],
                    [(50000078, 0), (50000078, 1), (50000078, 2)],
                    [(90000015, 0), (90000015, 1), (90000015, 2)],
                ],
            },
            {
                "id": 108,
                "name": "jubeat大回顧展 ROOM 8",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 4,
                "score": 2800000,
                "music": [
                    [(80000028, 0), (80000028, 1), (80000028, 2)],
                    [(80000087, 0), (80000087, 1), (80000087, 2)],
                    [(90000018, 0), (90000018, 1), (90000018, 2)],
                ],
            },
            {
                "id": 109,
                "name": "jubeat大回顧展 ROOM 9",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 4,
                "score": 930000,
                "music": [
                    [(10000038, 0), (10000038, 1), (10000038, 2)],
                    [(10000065, 0), (10000065, 1), (10000065, 2)],
                    [(90000010, 0), (90000010, 1), (90000010, 2)],
                ],
            },
            # MURU CUP
            {
                "id": 21,
                "name": "黒船来航",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 7,
                "score": 850000,
                "music": [
                    [(50000086, 2), (60000066, 2), (80000040, 1)],
                    [(50000096, 2), (80000048, 2)],
                    [(50000091, 2)],
                ],
            },
            {
                "id": 22,
                "name": "【五段】濁流を乗り越えて",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 7,
                "score": 2650000,
                "music": [
                    [
                        (50000343, 2),
                        (60000060, 2),
                        (70000156 if dataver < 2020040300 else 60000071, 2),
                    ],
                    [(60000027, 2), (80000048, 2)],
                    [(20000038, 2)],
                ],
            },
            {
                "id": 23,
                "name": "のんびり。ゆったり。ほがらかに。",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 8,
                "score": 950000,
                "music": [
                    [
                        (40000154, 2),
                        (80000124, 2),
                        (80000126 if dataver < 2021020200 else 90000139, 2),
                    ],
                    [
                        (60000048, 2),
                        (70000157 if dataver < 2020040300 else 80000041, 2),
                    ],
                    [(90000050, 2)],
                ],
            },
            {
                "id": 24,
                "name": "海・KOI・スィニョーレ！！",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 8,
                "score": 2650000,
                "music": [
                    [(50000201, 2)],
                    [(50000339, 2)],
                    [(50000038, 2)],
                ],
            },
            {
                "id": 25,
                "name": "【六段】電柱を見ると思出す",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 9,
                "score": 2750000,
                "music": [
                    [(50000288, 2), (80000046, 2), (80001008, 2)],
                    [(50000207, 2), (70000117, 2)],
                    [(30000048, 2)],
                ],
            },
            {
                "id": 26,
                "name": "雨上がりレインボー",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 9,
                "score": 2650000,
                "music": [
                    [(50000138, 2)],
                    [(80000057, 2)],
                    [(90000011, 2)],
                ],
            },
            {
                "id": 27,
                "name": "Rain時々雨ノチ雨",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 9,
                "score": 2650000,
                "music": [
                    [(30000050, 2)],
                    [(80000123, 2)],
                    [(50000092, 2)],
                ],
            },
            {
                "id": 28,
                "name": "#心に残った曲",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 7,
                "score": 2700000,
                "music": [
                    [(80000136, 0), (80000136, 1), (80000136, 2)],
                    [(20000038, 0), (20000038, 1), (20000038, 2)],
                    [(60000065, 0), (60000065, 1), (70000084, 1)],
                ],
            },
            # SAZAE CUP
            {
                "id": 31,
                "name": "超フェスタ！",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 10,
                "score": 930000,
                "music": [
                    [(70000076, 2), (70000077, 2)],
                    [(20000038, 2), (40000160, 2)],
                    [(70000145, 2)],
                ],
            },
            {
                "id": 32,
                "name": "【七段】操り人形はほくそ笑む",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 10,
                "score": 2800000,
                "music": [
                    [(70000006, 2), (70000171, 2), (80000003, 2)],
                    [(50000078, 2), (50000324, 2)],
                    [(80000118, 2)],
                ],
            },
            {
                "id": 33,
                "name": "絶体絶命スリーチャレンジ！",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_HAZARD,
                "hazard_type": self.COURSE_HAZARD_FC3,
                "difficulty": 11,
                "music": [
                    [(50000238, 2), (70000003, 2), (90000051, 1)],
                    [(50000027, 2), (50000387, 2)],
                    [(80000056, 2)],
                ],
            },
            {
                "id": 34,
                "name": "天国の舞踏会",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 11,
                "score": 2800000,
                "music": [
                    [(60000065, 1)],
                    [(80001007, 2)],
                    [(90001007, 2)],
                ],
            },
            {
                "id": 35,
                "name": "【八段】山の賽子",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 12,
                "score": 2820000,
                "music": [
                    [(50000200, 2), (50000291, 2), (60000003, 2)],
                    [(50000129, 2), (80000021, 2)],
                    [(80000087, 2)],
                ],
            },
            {
                "id": 36,
                "name": "#コクがある曲",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 12,
                "score": 2400000,
                "music": [
                    [(50000139, 0), (50000139, 1), (50000139, 2)],
                    [(90000002, 0), (90000002, 1), (90000002, 2)],
                    [(50000060, 0), (50000060, 1), (50000060, 2)],
                ],
            },
            # HOTATE CUP
            *kac_8th,  # Contains courses 41 and 42.
            {
                "id": 43,
                "name": "BEMANI MASTER KOREA 2019",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "hard": True,
                "difficulty": 14,
                "score": 700000,
                "music": [
                    [(90000003, 2)],
                    [(80000090, 2)],
                    [(90000009, 2)],
                ],
            },
            *kac_9th,  # Contains courses 201, 202, 203 and 204.
            *kac_10th,  # Contains courses 205 and 206.
            {
                "id": 207,
                "name": "#どうやって押してる？",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 13,
                "score": 2600000,
                "music": [
                    [(40000127, 0)],
                    [(50000123, 0)],
                    [(50000126, 0)],
                ],
            },
            {
                "id": 208,
                "name": "BEMANI MASTER KOREA 2021",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "hard": True,
                "difficulty": 14,
                "score": 700000,
                "music": [
                    [(90000180, 2)],
                    [(90000095, 2)],
                    [(90000047, 2)],
                ],
            },
            {
                "id": 44,
                "name": "初めてのHARD MODE再び",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 13,
                "score": 2750000,
                "music": [
                    [(50000096, 2), (50000263, 2), (80000119, 2)],
                    [(60000021, 2), (60000075, 2)],
                    [(60000039, 2)],
                ],
            },
            {
                "id": 45,
                "name": "【九段】2人からの挑戦状",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 13,
                "score": 2830000,
                "music": [
                    [(50000023, 2), (80000025, 2), (80000106, 2)],
                    [(50000124, 2), (80000082, 2)],
                    [(60000115, 2)],
                ],
            },
            {
                "id": 46,
                "name": "天空の庭　太陽の園",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "difficulty": 13,
                "score": 965000,
                "music": [
                    [(40000153, 2)],
                    [(80000007, 2)],
                    [(70000173, 2)],
                ],
            },
            {
                "id": 47,
                "name": "緊急！迅速！大混乱！",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 14,
                "score": 2900000,
                "music": [
                    [(20000040, 2), (50000244, 2), (60000074, 2)],
                    [(40000152, 2), (50000158, 2)],
                    [(40000057, 2)],
                ],
            },
            {
                "id": 48,
                "name": "【十段】時の超越者",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 14,
                "score": 2820000,
                "music": [
                    [(20000051, 2), (50000249, 2), (70000145, 2)],
                    [(40000046, 2), (50000180, 2)],
                    [(50000134, 2)],
                ],
            },
            {
                "id": 49,
                "name": "【伝導】10代目最強に挑戦！",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 14,
                "score": 2998179,
                "music": [
                    [(50000100, 2)],
                    [(90000047, 2)],
                    [(90000057, 2)],
                ],
            },
            {
                "id": 110,
                "name": "jubeat大回顧展 ROOM 10",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "difficulty": 13,
                "score": 2850000,
                "music": [
                    [(30000127, 2)],
                    [(60000078, 2)],
                    [(90000047, 2)],
                ],
            },
            # OSHAKO CUP
            {
                "id": 51,
                "name": "【皆伝】甘味なのに甘くない",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 15,
                "score": 2850000,
                "music": [
                    [(90000010, 2)],
                    [(80000101, 2)],
                    [(50000102, 2)],
                ],
            },
            {
                "id": 52,
                "name": "【伝導】真の青が魅せた空",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_SCORE,
                "hard": True,
                "difficulty": 15,
                "score": 970000,
                "music": [
                    [(50000332, 2)],
                    [(70000098, 2)],
                    [(90001005, 2)],
                ],
            },
            {
                "id": 53,
                "name": "豪華絢爛高揚絶頂",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 16,
                "score": 2960000,
                "music": [
                    [(10000065, 2)],
                    [(50000323, 2)],
                    [(50000208, 2)],
                ],
            },
            {
                "id": 54,
                "name": "絢爛豪華激情無常",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 16,
                "score": 2960000,
                "music": [
                    [(60000010, 2)],
                    [(70000110, 2)],
                    [(90000047, 2)],
                ],
            },
            {
                "id": 55,
                "name": "【指神】王の降臨",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 16,
                "score": 2980000,
                "music": [
                    [(70000094, 2)],
                    [(80000088, 2)],
                    [(70000110, 2)],
                ],
            },
            {
                "id": 56,
                "name": "【伝導】1116全てを超越した日",
                "course_type": self.COURSE_TYPE_PERMANENT,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 16,
                "score": 2975000,
                "music": [
                    [(50000208, 2)],
                    [(80000050, 2)],
                    [(90000057, 2)],
                ],
            },
            *random_course,  # Contains course 57.
            {
                "id": 58,
                "name": "#あなたのjubeatはどこから？",
                "course_type": self.COURSE_TYPE_TIME_BASED,
                "end_time": Time.end_of_this_week() + Time.SECONDS_IN_WEEK,
                "clear_type": self.COURSE_CLEAR_COMBINED_SCORE,
                "hard": True,
                "difficulty": 15,
                "score": 2900000,
                "music": [
                    [(10000065, 0), (10000065, 1), (10000065, 2)],
                    [(30000048, 0), (30000048, 1), (30000048, 2)],
                    [(90000047, 0), (90000047, 1), (90000047, 2)],
                ],
            },
        ]

    def __get_global_info(self) -> Node:
        info = Node.void("info")

        # Event info.
        event_info = Node.void("event_info")
        info.add_child(event_info)
        for event in self.EVENTS:
            evt = Node.void("event")
            event_info.add_child(evt)
            evt.set_attribute("type", str(event))
            evt.add_child(Node.u8("state", 1 if self.EVENTS[event]["enabled"] else 0))

        genre_def_music = Node.void("genre_def_music")
        info.add_child(genre_def_music)

        info.add_child(
            Node.s32_array(
                "black_jacket_list",
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],
            )
        )

        # Mapping of what music is allowed by default, if this is set to all 0's
        # then the game will crash because it can't figure out what default song
        # to choose for new player sort. The way to calculate what song one of the
        # bits is for in any music_list array below is to look at the "pos_index"
        # field in the music_info.xml file. The entry in the array is calculated by
        # "(pos_index / 32)" and the bit to set is "1 << (pos_index % 32)"
        info.add_child(
            Node.s32_array(
                "white_music_list",
                [
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                ],
            )
        )

        # Mapping of what markers and themes are allowed for profile customization
        # by default. If this is set to all 0's then there are no markers or themes
        # offered and the default marker is forced.
        info.add_child(
            Node.s32_array(
                "white_marker_list",
                [
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                ],
            )
        )

        info.add_child(
            Node.s32_array(
                "white_theme_list",
                [
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                    -1,
                ],
            )
        )

        # Possibly default unlocks for songs. Need to investigate further.
        info.add_child(
            Node.s32_array(
                "open_music_list",
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],
            )
        )

        info.add_child(
            Node.s32_array(
                "shareable_music_list",
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],
            )
        )

        # Bitfield that determines what music is considered "pick up" versus "common" on the jubility
        # jacket field at the end of the game. Hot music is just the music in the current mix. The
        # bitfield values were taken from the "pos_index" field for all songs that are in festo.
        info.add_child(
            Node.s32_array(
                "hot_music_list",
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    -4194304,
                    -2080769,
                    -1,
                    -17,
                    -3,
                    -33554433,
                    -242,
                    -268435473,
                    1073741823,
                    -1073748992,
                    15,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],
            )
        )

        jbox = Node.void("jbox")
        info.add_child(jbox)
        jbox.add_child(Node.s32("point", 0))
        emblem = Node.void("emblem")
        jbox.add_child(emblem)
        normal = Node.void("normal")
        emblem.add_child(normal)
        premium = Node.void("premium")
        emblem.add_child(premium)
        normal.add_child(Node.s16("index", 2))
        premium.add_child(Node.s16("index", 1))

        born = Node.void("born")
        info.add_child(born)
        born.add_child(Node.s8("status", 0))
        born.add_child(Node.s16("year", 0))

        game_config = self.get_game_config()
        konami_logo_50th = Node.void("konami_logo_50th")
        info.add_child(konami_logo_50th)
        konami_logo_50th.add_child(Node.bool("is_available", game_config.get_bool("50th_anniversary")))

        expert_option = Node.void("expert_option")
        info.add_child(expert_option)
        expert_option.add_child(Node.bool("is_available", True))

        all_music_matching = Node.void("all_music_matching")
        info.add_child(all_music_matching)
        all_music_matching.add_child(Node.bool("is_available", True))

        department = Node.void("department")
        info.add_child(department)
        department.add_child(Node.void("shop_list"))

        question_list = Node.void("question_list")
        info.add_child(question_list)

        # Set up TUNE RUN course requirements
        clan_course_list = Node.void("course_list")
        info.add_child(clan_course_list)

        valid_courses: Set[int] = set()
        dataver = self.model.version or 2022052400
        for course in self.__get_course_list():
            if course["id"] < 1:
                raise Exception(f"Invalid course ID {course['id']} found in course list!")
            if course["id"] in valid_courses:
                raise Exception(f"Duplicate ID {course['id']} found in course list!")
            if course["clear_type"] == self.COURSE_CLEAR_HAZARD and "hazard_type" not in course:
                raise Exception(f"Need 'hazard_type' set in course {course['id']}!")
            if course["course_type"] == self.COURSE_TYPE_TIME_BASED and "end_time" not in course:
                raise Exception(f"Need 'end_time' set in course {course['id']}!")
            if (
                course["clear_type"] in [self.COURSE_CLEAR_SCORE, self.COURSE_CLEAR_COMBINED_SCORE]
                and "score" not in course
            ):
                raise Exception(f"Need 'score' set in course {course['id']}!")
            if course["clear_type"] == self.COURSE_CLEAR_SCORE and course["score"] > 1000000:
                raise Exception(f"Invalid per-coure score in course {course['id']}!")
            if course["clear_type"] == self.COURSE_CLEAR_COMBINED_SCORE and course["score"] <= 1000000:
                raise Exception(f"Invalid combined score in course {course['id']}!")
            valid_courses.add(course["id"])

            # Basics
            clan_course = Node.void("course")
            clan_course_list.add_child(clan_course)
            clan_course.set_attribute("release_code", str(dataver))
            clan_course.set_attribute("version_id", "0")
            clan_course.set_attribute("id", str(course["id"]))
            clan_course.set_attribute("course_type", str(course["course_type"]))
            clan_course.add_child(Node.s32("difficulty", course["difficulty"]))
            clan_course.add_child(Node.u64("etime", (course["end_time"] if "end_time" in course else 0) * 1000))
            clan_course.add_child(Node.string("name", course["name"]))

            # List of included songs
            tune_list = Node.void("tune_list")
            clan_course.add_child(tune_list)
            for order, charts in enumerate(course["music"]):
                tune = Node.void("tune")
                tune_list.add_child(tune)
                tune.set_attribute("no", str(order + 1))

                seq_list = Node.void("seq_list")
                tune.add_child(seq_list)

                for songid, chart in charts:
                    seq = Node.void("seq")
                    seq_list.add_child(seq)
                    seq.add_child(Node.s32("music_id", songid))
                    seq.add_child(Node.s32("difficulty", chart))
                    seq.add_child(Node.bool("is_secret", False))

            # Clear criteria
            clear = Node.void("clear")
            clan_course.add_child(clear)
            ex_option = Node.void("ex_option")
            clear.add_child(ex_option)
            ex_option.add_child(Node.bool("is_hard", course["hard"] if "hard" in course else False))
            ex_option.add_child(
                Node.s32(
                    "hazard_type",
                    course["hazard_type"] if "hazard_type" in course else 0,
                )
            )
            clear.set_attribute("type", str(course["clear_type"]))
            clear.add_child(Node.s32("score", course["score"] if "score" in course else 0))

            reward_list = Node.void("reward_list")
            clear.add_child(reward_list)

        # Each of the following two sections should have zero or more child nodes (no
        # particular name) which look like the following:
        #     <node>
        #         <id __type="s32">songid</id>
        #         <stime __type="str">start time?</stime>
        #         <etime __type="str">end time?</etime>
        #     </node>
        # Share music?
        share_music = Node.void("share_music")
        info.add_child(share_music)

        weekly_music = Node.void("weekly_music")
        info.add_child(weekly_music)
        weekly_music.add_child(Node.s32("value", 0))

        info.add_child(
            Node.s32_array(
                "add_default_music_list",
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],
            )
        )

        # The following section should have zero or more child nodes (no particular
        # name) which look like the following, with a song ID in the node's id attribute:
        #     <node id="" />
        weekly_music_list = Node.void("music_list")
        weekly_music.add_child(weekly_music_list)

        # Enable/disable festo dungeon.
        if game_config.get_bool("festo_dungeon"):
            festo_dungeon = Node.void("festo_dungeon")
            info.add_child(festo_dungeon)
            festo_dungeon.add_child(Node.u64("etime", (Time.now() + Time.SECONDS_IN_WEEK) * 1000))

        # Unsupported team_battle nodes.
        info.add_child(Node.void("team_battle"))

        # Unsupported EMO list for EMO shop.
        info.add_child(Node.void("emo_list"))

        # Unsupported hike_event.
        info.add_child(Node.void("hike_event"))

        # Unsupported tip_list, this probably lets the server control the tips between songs.
        info.add_child(Node.void("tip_list"))

        # Unsupported mission travel event, which is very server-controlled.
        info.add_child(Node.void("travel"))

        # Unsupported stamp rally event, since this poorly undocumented.
        info.add_child(Node.void("stamp"))

        return info

    def handle_shopinfo_regist_request(self, request: Node) -> Node:
        # Update the name of this cab for admin purposes
        self.update_machine_name(request.child_value("shop/name"))

        shopinfo = Node.void("shopinfo")

        data = Node.void("data")
        shopinfo.add_child(data)
        data.add_child(Node.u32("cabid", 1))
        data.add_child(Node.string("locationid", "nowhere"))
        data.add_child(Node.u8("tax_phase", 1))

        facility = Node.void("facility")
        data.add_child(facility)
        facility.add_child(Node.u32("exist", 1))

        data.add_child(self.__get_global_info())

        return shopinfo

    def handle_demodata_get_info_request(self, request: Node) -> Node:
        root = Node.void("demodata")
        data = Node.void("data")
        root.add_child(data)

        info = Node.void("info")
        data.add_child(info)

        # This is the same stuff set in the common info, so if we ever do make this
        # configurable, I think we'll need to return the same thing in both spots.
        info.add_child(
            Node.s32_array(
                "black_jacket_list",
                [
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                ],
            )
        )

        return root

    def handle_demodata_get_jbox_list_request(self, request: Node) -> Node:
        root = Node.void("demodata")
        return root

    def handle_jbox_get_agreement_request(self, request: Node) -> Node:
        root = Node.void("jbox")
        root.add_child(Node.bool("is_agreement", True))
        return root

    def handle_jbox_get_list_request(self, request: Node) -> Node:
        root = Node.void("jbox")
        root.add_child(Node.void("selection_list"))
        return root

    def handle_ins_netlog_request(self, request: Node) -> Node:
        root = Node.void("ins")
        return root

    def handle_lab_get_ranking_request(self, request: Node) -> Node:
        root = Node.void("lab")
        root.add_child(Node.s8("category", request.child_value("category") or 0))

        entries = Node.void("entries")
        root.add_child(entries)

        # The game allows up to 10 entries, each looking like this:
        #     <entry>
        #         <seq_id __type="str">XXXXXXXXX</seq_id>
        #     <entry>
        entries.set_attribute("count", "0")

        return root

    def handle_recommend_get_recommend_request(self, request: Node) -> Node:
        recommend = Node.void("recommend")
        data = Node.void("data")
        recommend.add_child(data)

        player = Node.void("player")
        data.add_child(player)
        music_list = Node.void("music_list")
        player.add_child(music_list)

        # TODO: Might be a way to figure out who plays what song and then offer
        # recommendations based on that. There should be 12 songs returned here.
        recommended_songs: List[Song] = []
        for i, song in enumerate(recommended_songs):
            music = Node.void("music")
            music_list.add_child(music)
            music.set_attribute("order", str(i))
            music.add_child(Node.s32("music_id", song.id))
            music.add_child(Node.s8("seq", song.chart))

        return recommend

    def handle_gametop_get_info_request(self, request: Node) -> Node:
        root = Node.void("gametop")
        data = Node.void("data")
        root.add_child(data)
        data.add_child(self.__get_global_info())

        return root

    def handle_gametop_regist_request(self, request: Node) -> Node:
        data = request.child("data")
        player = data.child("player")
        refid = player.child_value("refid")
        name = player.child_value("name")
        root = self.new_profile_by_refid(refid, name)
        return root

    def handle_gametop_get_pdata_request(self, request: Node) -> Node:
        data = request.child("data")
        player = data.child("player")
        refid = player.child_value("refid")
        root = self.get_profile_by_refid(refid)
        if root is None:
            root = Node.void("gametop")
            root.set_attribute("status", str(Status.NO_PROFILE))
        return root

    def handle_gametop_get_mdata_request(self, request: Node) -> Node:
        data = request.child("data")
        player = data.child("player")
        extid = player.child_value("jid")
        mdata_ver = player.child_value("mdata_ver")
        root = self.get_scores_by_extid(extid, mdata_ver, 3)
        if root is None:
            root = Node.void("gametop")
            root.set_attribute("status", str(Status.NO_PROFILE))
        return root

    def handle_gameend_final_request(self, request: Node) -> Node:
        data = request.child("data")
        player = data.child("player")

        if player is not None:
            refid = player.child_value("refid")
        else:
            refid = None

        if refid is not None:
            userid = self.data.remote.user.from_refid(self.game, self.version, refid)
        else:
            userid = None

        if userid is not None:
            profile = self.get_profile(userid)

            # Grab unlock progress
            item = player.child("item")
            if item is not None:
                owned_emblems = self.calculate_owned_items(item.child_value("emblem_list"))
                for index in owned_emblems:
                    self.data.local.user.put_achievement(
                        self.game,
                        self.version,
                        userid,
                        index,
                        "emblem",
                        {},
                    )

            # jbox stuff
            jbox = player.child("jbox")
            jboxdict = profile.get_dict("jbox")
            if jbox is not None:
                jboxdict.replace_int("point", jbox.child_value("point"))
                emblemtype = jbox.child_value("emblem/type")
                index = jbox.child_value("emblem/index")
                if emblemtype == self.JBOX_EMBLEM_NORMAL:
                    jboxdict.replace_int("normal_index", index)
                elif emblemtype == self.JBOX_EMBLEM_PREMIUM:
                    jboxdict.replace_int("premium_index", index)
            profile.replace_dict("jbox", jboxdict)

            # Born stuff
            born = player.child("born")
            if born is not None:
                profile.replace_int("born_status", born.child_value("status"))
                profile.replace_int("born_year", born.child_value("year"))
        else:
            profile = None

        if userid is not None and profile is not None:
            self.put_profile(userid, profile)

        return Node.void("gameend")

    def format_scores(self, userid: UserID, profile: Profile, scores: List[Score]) -> Node:
        root = Node.void("gametop")
        datanode = Node.void("data")
        root.add_child(datanode)
        player = Node.void("player")
        datanode.add_child(player)
        player.add_child(Node.s32("jid", profile.extid))
        playdata = Node.void("mdata_list")
        player.add_child(playdata)

        music = ValidatedDict()
        for score in scores:
            chart = self.db_to_game_chart(score.chart)
            if score.chart in {
                self.CHART_TYPE_HARD_BASIC,
                self.CHART_TYPE_HARD_ADVANCED,
                self.CHART_TYPE_HARD_EXTREME,
            }:
                prefix = "hard"
            else:
                prefix = "normal"

            data = music.get_dict(str(score.id))
            play_cnt = data.get_int_array(f"{prefix}_play_cnt", 3)
            clear_cnt = data.get_int_array(f"{prefix}_clear_cnt", 3)
            clear_flags = data.get_int_array(f"{prefix}_clear_flags", 3)
            fc_cnt = data.get_int_array(f"{prefix}_fc_cnt", 3)
            ex_cnt = data.get_int_array(f"{prefix}_ex_cnt", 3)
            points = data.get_int_array(f"{prefix}_points", 3)
            music_rate = data.get_int_array(f"{prefix}_music_rate", 3)

            # Replace data for this chart type
            play_cnt[chart] = score.plays
            clear_cnt[chart] = score.data.get_int("clear_count")
            fc_cnt[chart] = score.data.get_int("full_combo_count")
            ex_cnt[chart] = score.data.get_int("excellent_count")
            points[chart] = score.points
            music_rate[chart] = score.data.get_int("music_rate")

            # Format the clear flags
            clear_flags[chart] = self.GAME_FLAG_BIT_PLAYED
            if score.data.get_int("clear_count") > 0:
                clear_flags[chart] |= self.GAME_FLAG_BIT_CLEARED
            if score.data.get_int("full_combo_count") > 0:
                clear_flags[chart] |= self.GAME_FLAG_BIT_FULL_COMBO
            if score.data.get_int("excellent_count") > 0:
                clear_flags[chart] |= self.GAME_FLAG_BIT_EXCELLENT

            # Save chart data back
            data.replace_int_array(f"{prefix}_play_cnt", 3, play_cnt)
            data.replace_int_array(f"{prefix}_clear_cnt", 3, clear_cnt)
            data.replace_int_array(f"{prefix}_clear_flags", 3, clear_flags)
            data.replace_int_array(f"{prefix}_fc_cnt", 3, fc_cnt)
            data.replace_int_array(f"{prefix}_ex_cnt", 3, ex_cnt)
            data.replace_int_array(f"{prefix}_points", 3, points)
            data.replace_int_array(f"{prefix}_music_rate", 3, music_rate)

            # Update the ghost (untyped)
            ghost = data.get(f"{prefix}_ghost", [None, None, None])
            ghost[chart] = score.data.get("ghost")
            data[f"{prefix}_ghost"] = ghost

            # Save it back
            music.replace_dict(str(score.id), data)

        for scoreid in music:
            scoredata = music.get_dict(scoreid)
            musicdata = Node.void("musicdata")
            playdata.add_child(musicdata)
            musicdata.set_attribute("music_id", scoreid)

            # Since in the worst case, we could be wasting a lot of data by always sending both a normal and hard mode block
            # we need to check if there's even a score array worth sending. This should help with performance for larger
            # score databases.
            if scoredata.get_int_array("normal_play_cnt", 3) != [0, 0, 0]:
                normalnode = Node.void("normal")
                musicdata.add_child(normalnode)

                normalnode.add_child(Node.s32_array("play_cnt", scoredata.get_int_array("normal_play_cnt", 3)))
                normalnode.add_child(Node.s32_array("clear_cnt", scoredata.get_int_array("normal_clear_cnt", 3)))
                normalnode.add_child(Node.s32_array("fc_cnt", scoredata.get_int_array("normal_fc_cnt", 3)))
                normalnode.add_child(Node.s32_array("ex_cnt", scoredata.get_int_array("normal_ex_cnt", 3)))
                normalnode.add_child(Node.s32_array("score", scoredata.get_int_array("normal_points", 3)))
                normalnode.add_child(Node.s8_array("clear", scoredata.get_int_array("normal_clear_flags", 3)))
                normalnode.add_child(Node.s32_array("music_rate", scoredata.get_int_array("normal_music_rate", 3)))

                for i, ghost in enumerate(scoredata.get("normal_ghost", [None, None, None])):
                    if ghost is None:
                        continue

                    bar = Node.u8_array("bar", ghost)
                    normalnode.add_child(bar)
                    bar.set_attribute("seq", str(i))

            if scoredata.get_int_array("hard_play_cnt", 3) != [0, 0, 0]:
                hardnode = Node.void("hard")
                musicdata.add_child(hardnode)

                hardnode.add_child(Node.s32_array("play_cnt", scoredata.get_int_array("hard_play_cnt", 3)))
                hardnode.add_child(Node.s32_array("clear_cnt", scoredata.get_int_array("hard_clear_cnt", 3)))
                hardnode.add_child(Node.s32_array("fc_cnt", scoredata.get_int_array("hard_fc_cnt", 3)))
                hardnode.add_child(Node.s32_array("ex_cnt", scoredata.get_int_array("hard_ex_cnt", 3)))
                hardnode.add_child(Node.s32_array("score", scoredata.get_int_array("hard_points", 3)))
                hardnode.add_child(Node.s8_array("clear", scoredata.get_int_array("hard_clear_flags", 3)))
                hardnode.add_child(Node.s32_array("music_rate", scoredata.get_int_array("hard_music_rate", 3)))

                for i, ghost in enumerate(scoredata.get("hard_ghost", [None, None, None])):
                    if ghost is None:
                        continue

                    bar = Node.u8_array("bar", ghost)
                    hardnode.add_child(bar)
                    bar.set_attribute("seq", str(i))

        return root

    def format_profile(self, userid: UserID, profile: Profile) -> Node:
        root = Node.void("gametop")
        data = Node.void("data")
        root.add_child(data)

        # Figure out if we're force-unlocking songs.
        game_config = self.get_game_config()
        force_unlock = game_config.get_bool("force_song_unlock")

        # Calculate all of our achievement-backed entities.
        achievements = self.data.local.user.get_achievements(self.game, self.version, userid)
        owned_songs: Set[int] = set()
        owned_secrets: Set[int] = set()
        owned_emblems: Set[int] = set()
        event_completion: Dict[int, bool] = {}
        course_completion: Dict[int, ValidatedDict] = {}
        for achievement in achievements:
            if achievement.type == "event":
                event_completion[achievement.id] = achievement.data.get_bool("is_completed")
            elif achievement.type == "course":
                course_completion[achievement.id] = achievement.data
            elif achievement.type == "emblem":
                owned_emblems.add(achievement.id)
            elif achievement.type == "song":
                owned_songs.add(achievement.id)
            elif achievement.type == "secret":
                owned_secrets.add(achievement.id)

        # Make sure we grant ownership of default main parts.
        default_emblems = self.default_select_jbox()
        owned_emblems.update(default_emblems)
        default_main = next(iter(default_emblems)) if default_emblems else 0

        # Jubeat Clan appears to allow full event overrides per-player
        data.add_child(self.__get_global_info())

        player = Node.void("player")
        data.add_child(player)

        # Basic profile info
        player.add_child(Node.string("name", profile.get_str("name", "なし")))
        player.add_child(Node.s32("jid", profile.extid))

        # Miscelaneous crap
        player.add_child(Node.s32("session_id", 1))
        player.add_child(Node.u64("event_flag", profile.get_int("event_flag")))

        # Player info and statistics
        info = Node.void("info")
        player.add_child(info)
        info.add_child(Node.s32("tune_cnt", profile.get_int("tune_cnt")))
        info.add_child(Node.s32("save_cnt", profile.get_int("save_cnt")))
        info.add_child(Node.s32("saved_cnt", profile.get_int("saved_cnt")))
        info.add_child(Node.s32("fc_cnt", profile.get_int("fc_cnt")))
        info.add_child(Node.s32("ex_cnt", profile.get_int("ex_cnt")))
        info.add_child(Node.s32("clear_cnt", profile.get_int("clear_cnt")))
        info.add_child(Node.s32("match_cnt", profile.get_int("match_cnt")))
        info.add_child(Node.s32("beat_cnt", profile.get_int("beat_cnt")))
        info.add_child(Node.s32("mynews_cnt", profile.get_int("mynews_cnt")))
        info.add_child(Node.s32("mtg_entry_cnt", profile.get_int("mtg_entry_cnt")))
        info.add_child(Node.s32("mtg_hold_cnt", profile.get_int("mtg_hold_cnt")))
        info.add_child(Node.u8("mtg_result", profile.get_int("mtg_result")))
        info.add_child(Node.s32("bonus_tune_points", profile.get_int("bonus_tune_points")))
        info.add_child(Node.bool("is_bonus_tune_played", profile.get_bool("is_bonus_tune_played")))

        # Looks to be set to true when there's an old profile, stops tutorial from
        # happening on first load.
        info.add_child(
            Node.bool(
                "inherit",
                profile.get_bool("has_old_version") and not profile.get_bool("saved"),
            )
        )

        # Last played data, for showing cursor and such
        lastdict = profile.get_dict("last")
        last = Node.void("last")
        player.add_child(last)
        last.add_child(Node.s64("play_time", lastdict.get_int("play_time")))
        last.add_child(Node.string("shopname", lastdict.get_str("shopname")))
        last.add_child(Node.string("areaname", lastdict.get_str("areaname")))
        last.add_child(Node.s32("music_id", lastdict.get_int("music_id")))
        last.add_child(Node.s8("seq_id", lastdict.get_int("seq_id")))
        last.add_child(Node.s8("sort", lastdict.get_int("sort")))
        last.add_child(Node.s8("category", lastdict.get_int("category")))
        last.add_child(Node.s8("expert_option", lastdict.get_int("expert_option")))

        settings = Node.void("settings")
        last.add_child(settings)
        settings.add_child(Node.s8("marker", lastdict.get_int("marker")))
        settings.add_child(Node.s8("theme", lastdict.get_int("theme")))
        settings.add_child(Node.s16("title", lastdict.get_int("title")))
        settings.add_child(Node.s16("parts", lastdict.get_int("parts")))
        settings.add_child(Node.s8("rank_sort", lastdict.get_int("rank_sort")))
        settings.add_child(Node.s8("combo_disp", lastdict.get_int("combo_disp")))
        settings.add_child(Node.s8("matching", lastdict.get_int("matching")))
        settings.add_child(Node.s8("hard", lastdict.get_int("hard")))
        settings.add_child(Node.s8("hazard", lastdict.get_int("hazard")))

        # Hack to make the default emblem appear properly.
        partslist = lastdict.get_int_array("emblem", 5, [0, default_main, 0, 0, 0])
        if partslist[1] == 0:
            partslist[1] = default_main
        settings.add_child(Node.s16_array("emblem", partslist))

        item = Node.void("item")
        player.add_child(item)

        # Default music availability, I think? The game doesn't seem to make much use of this, so I think
        # we can safely set it to all 1's much like we do the open_music_list bitfield in global settings.
        item.add_child(Node.s32_array("music_list", profile.get_int_array("music_list", 64, [-1] * 64)))

        # Song unlocks, force everything on if force unlocked, otherwise default to what the game granted.
        item.add_child(
            Node.s32_array(
                "secret_list",
                ([-1] * 64) if force_unlock else self.create_owned_items(owned_songs, 64),
            )
        )

        # We force unlock all themes, markers, titles, and parts, regardless of what the client ended up earning.
        item.add_child(Node.s32_array("theme_list", profile.get_int_array("theme_list", 16, [-1] * 16)))
        item.add_child(Node.s32_array("marker_list", profile.get_int_array("marker_list", 16, [-1] * 16)))
        item.add_child(Node.s32_array("title_list", profile.get_int_array("title_list", 160, [-1] * 160)))
        item.add_child(Node.s32_array("parts_list", profile.get_int_array("parts_list", 160, [-1] * 160)))

        # These get earned by unlocking them through JBOX.
        item.add_child(Node.s32_array("emblem_list", self.create_owned_items(owned_emblems, 96)))

        # I got no idea wtf this is, so I'm defaulting it to all on like the above ones.
        item.add_child(Node.s32_array("commu_list", profile.get_int_array("commu_list", 16, [-1] * 16)))

        # I have no idea what these are for. I figured it was for the server to grant songs/themes/markers
        # outside of gameplay, but the game doesn't seem to react to setting values here. So, lets set them
        # to all 1's and move on. Tracing the handling of this shows that the game usually sets the same bit
        # in both the secret list above and this one, and doesn't seem to care about parsing the values as
        # they come in.
        new = Node.void("new")
        item.add_child(new)
        new.add_child(
            Node.s32_array(
                "secret_list",
                ([-1] * 64) if force_unlock else self.create_owned_items(owned_secrets, 64),
            )
        )
        new.add_child(Node.s32_array("theme_list", profile.get_int_array("theme_list_new", 16, [-1] * 16)))
        new.add_child(Node.s32_array("marker_list", profile.get_int_array("marker_list_new", 16, [-1] * 16)))

        # Add rivals to profile.
        rivallist = Node.void("rivallist")
        player.add_child(rivallist)

        links = self.data.local.user.get_links(self.game, self.version, userid)
        rivalcount = 0
        for link in links:
            if link.type != "rival":
                continue

            rprofile = self.get_profile(link.other_userid)
            if rprofile is None:
                continue

            rival = Node.void("rival")
            rivallist.add_child(rival)
            rival.add_child(Node.s32("jid", rprofile.extid))
            rival.add_child(Node.string("name", rprofile.get_str("name")))

            # This looks like a carry-over from prop's career and isn't displayed.
            career = Node.void("career")
            rival.add_child(career)
            career.add_child(Node.s16("level", 1))

            # Lazy way of keeping track of rivals, since we can only have 3
            # or the game with throw up.
            rivalcount += 1
            if rivalcount >= 3:
                break

        lab_edit_seq = Node.void("lab_edit_seq")
        player.add_child(lab_edit_seq)
        lab_edit_seq.set_attribute("count", "0")

        # Full combo challenge
        entry = self.data.local.game.get_time_sensitive_settings(self.game, self.version, "fc_challenge")
        if entry is None:
            entry = ValidatedDict()

        # Figure out if we've played these songs
        start_time, end_time = self.data.local.network.get_schedule_duration("daily")
        today_attempts = self.data.local.music.get_all_attempts(
            self.game,
            self.music_version,
            userid,
            entry.get_int("today", -1),
            timelimit=start_time,
        )
        whim_attempts = self.data.local.music.get_all_attempts(
            self.game,
            self.music_version,
            userid,
            entry.get_int("whim", -1),
            timelimit=start_time,
        )

        # Full combo challenge and whim challenge
        fc_challenge = Node.void("fc_challenge")
        player.add_child(fc_challenge)
        today = Node.void("today")
        fc_challenge.add_child(today)
        today.add_child(Node.s32("music_id", entry.get_int("today", -1)))
        today.add_child(Node.u8("state", 0x40 if len(today_attempts) > 0 else 0x0))
        whim = Node.void("whim")
        fc_challenge.add_child(whim)
        whim.add_child(Node.s32("music_id", entry.get_int("whim", -1)))
        whim.add_child(Node.u8("state", 0x40 if len(whim_attempts) > 0 else 0x0))

        # No news, ever.
        official_news = Node.void("official_news")
        player.add_child(official_news)
        news_list = Node.void("news_list")
        official_news.add_child(news_list)

        # Sane defaults for unknown/who cares nodes
        history = Node.void("history")
        player.add_child(history)
        history.set_attribute("count", "0")

        free_first_play = Node.void("free_first_play")
        player.add_child(free_first_play)
        free_first_play.add_child(Node.bool("is_available", False))

        # Player status for events
        event_info = Node.void("event_info")
        player.add_child(event_info)
        for eventid, eventdata in self.EVENTS.items():
            # There are two significant bits here, bit 0 and bit 1, I think the first
            # one is whether the event is started, second is if its finished?
            event = Node.void("event")
            event_info.add_child(event)
            event.set_attribute("type", str(eventid))

            state = 0x0
            state |= self.EVENT_STATUS_OPEN if eventdata["enabled"] else 0
            state |= self.EVENT_STATUS_COMPLETE if event_completion.get(eventid, False) else 0
            event.add_child(Node.u8("state", state))

        # JBox stuff
        jbox = Node.void("jbox")
        jboxdict = profile.get_dict("jbox")
        player.add_child(jbox)
        jbox.add_child(Node.s32("point", jboxdict.get_int("point")))
        emblem = Node.void("emblem")
        jbox.add_child(emblem)
        normal = Node.void("normal")
        emblem.add_child(normal)
        premium = Node.void("premium")
        emblem.add_child(premium)

        # Calculate a random index for normal and premium to give to player
        # as a gatcha.
        normalindex, premiumindex = self.random_select_jbox(owned_emblems)
        normal.add_child(Node.s16("index", normalindex))
        premium.add_child(Node.s16("index", premiumindex))

        # New Music stuff
        new_music = Node.void("new_music")
        player.add_child(new_music)

        navi = Node.void("navi")
        player.add_child(navi)
        navi.add_child(Node.u64("flag", profile.get_int("navi_flag")))

        # Gift list, maybe from other players?
        gift_list = Node.void("gift_list")
        player.add_child(gift_list)
        # If we had gifts, they look like this. This is incomplete, however,
        # because I never bothered to find the virtual function to decode "detail".
        # Note that detail is only necessary if you don't want to give reason/id,
        # so its gotta be some hacked-on override.
        #     <gift reason="??" id="??">
        #         <detail>??</detail>
        #     </gift>

        # Birthday event?
        born = Node.void("born")
        player.add_child(born)
        born.add_child(Node.s8("status", profile.get_int("born_status")))
        born.add_child(Node.s16("year", profile.get_int("born_year")))

        # More crap
        question_list = Node.void("question_list")
        player.add_child(question_list)

        emo_list = Node.void("emo_list")
        player.add_child(emo_list)

        # Some server node
        server = Node.void("server")
        player.add_child(server)

        # Course List Progress
        course_list = Node.void("course_list")
        player.add_child(course_list)

        # Each course that we have completed has one of the following nodes.
        for course in self.__get_course_list():
            status_dict = course_completion.get(course["id"], ValidatedDict())
            status = 0
            status |= self.COURSE_STATUS_SEEN if status_dict.get_bool("seen") else 0
            status |= self.COURSE_STATUS_PLAYED if status_dict.get_bool("played") else 0
            status |= self.COURSE_STATUS_CLEARED if status_dict.get_bool("cleared") else 0

            coursenode = Node.void("course")
            course_list.add_child(coursenode)
            coursenode.set_attribute("id", str(course["id"]))
            coursenode.add_child(Node.s8("status", status))

        # For some reason, this is on the course list node this time around.
        category_list = Node.void("category_list")
        course_list.add_child(category_list)
        for categoryid in range(1, 7):
            category = Node.void("category")
            category_list.add_child(category)
            category.set_attribute("id", str(categoryid))
            category.add_child(Node.bool("is_display", True))

        # Fill in category
        fill_in_category = Node.void("fill_in_category")
        player.add_child(fill_in_category)

        normal = Node.void("normal")
        fill_in_category.add_child(normal)
        normal.add_child(
            Node.s32_array(
                "no_gray_flag_list",
                profile.get_int_array("normal_no_gray_flag_list", 16, [0] * 16),
            )
        )
        normal.add_child(
            Node.s32_array(
                "all_yellow_flag_list",
                profile.get_int_array("normal_all_yellow_flag_list", 16, [0] * 16),
            )
        )
        normal.add_child(
            Node.s32_array(
                "full_combo_flag_list",
                profile.get_int_array("normal_full_combo_flag_list", 16, [0] * 16),
            )
        )
        normal.add_child(
            Node.s32_array(
                "excellent_flag_list",
                profile.get_int_array("normal_excellent_flag_list", 16, [0] * 16),
            )
        )

        hard = Node.void("hard")
        fill_in_category.add_child(hard)
        hard.add_child(
            Node.s32_array(
                "no_gray_flag_list",
                profile.get_int_array("hard_no_gray_flag_list", 16, [0] * 16),
            )
        )
        hard.add_child(
            Node.s32_array(
                "all_yellow_flag_list",
                profile.get_int_array("hard_all_yellow_flag_list", 16, [0] * 16),
            )
        )
        hard.add_child(
            Node.s32_array(
                "full_combo_flag_list",
                profile.get_int_array("hard_full_combo_flag_list", 16, [0] * 16),
            )
        )
        hard.add_child(
            Node.s32_array(
                "excellent_flag_list",
                profile.get_int_array("hard_excellent_flag_list", 16, [0] * 16),
            )
        )

        # Unknown department shop stuff, I think this handles the EMO shop.
        department = Node.void("department")
        player.add_child(department)
        department.add_child(Node.void("shop_list"))

        # Stamp rally stuff, this is too server-controlled and not documented on BemaniWiki.
        stamp = Node.void("stamp")
        player.add_child(stamp)
        stamp.add_child(Node.void("sheet_list"))

        # Missing team_battle, which we do not support.

        # Missing eamuse_gift_list, which we do not support.

        # Missing hike_event, which I can't find any info on.

        # Festo dungeon
        festo_dungeon = Node.void("festo_dungeon")
        player.add_child(festo_dungeon)
        festo_dungeon.add_child(Node.s32("phase", profile.get_int("festo_dungeon_phase")))
        festo_dungeon.add_child(Node.s32("clear_flag", profile.get_int("festo_dungeon_clear_flag")))

        # Missing travel event, which I do not want to implement.

        return root

    def unformat_profile(self, userid: UserID, request: Node, oldprofile: Profile) -> Profile:
        newprofile = oldprofile.clone()
        newprofile.replace_bool("saved", True)
        data = request.child("data")

        # Figure out if we're force-unlocking songs. If we are, we don't want to persist
        # secret stuff otherwise the game will accidentally unlock everything in the profile.
        game_config = self.get_game_config()
        force_unlock = game_config.get_bool("force_song_unlock")

        # Grab system information
        sysinfo = data.child("info")

        # Grab player information
        player = data.child("player")

        # Grab result information
        result = data.child("result")

        # Grab last information. Lots of this will be filled in while grabbing scores
        last = newprofile.get_dict("last")
        if sysinfo is not None:
            last.replace_int("play_time", sysinfo.child_value("time_gameend"))
            last.replace_str("shopname", sysinfo.child_value("shopname"))
            last.replace_str("areaname", sysinfo.child_value("areaname"))

        # Grab player info for echoing back
        info = player.child("info")
        if info is not None:
            newprofile.replace_int("tune_cnt", info.child_value("tune_cnt"))
            newprofile.replace_int("save_cnt", info.child_value("save_cnt"))
            newprofile.replace_int("saved_cnt", info.child_value("saved_cnt"))
            newprofile.replace_int("fc_cnt", info.child_value("fc_cnt"))
            newprofile.replace_int("ex_cnt", info.child_value("ex_cnt"))
            newprofile.replace_int("clear_cnt", info.child_value("clear_cnt"))
            newprofile.replace_int("match_cnt", info.child_value("match_cnt"))
            newprofile.replace_int("beat_cnt", info.child_value("beat_cnt"))
            newprofile.replace_int("mynews_cnt", info.child_value("mynews_cnt"))

            newprofile.replace_int("bonus_tune_points", info.child_value("bonus_tune_points"))
            newprofile.replace_bool("is_bonus_tune_played", info.child_value("is_bonus_tune_played"))

        # Grab last settings
        lastnode = player.child("last")
        if lastnode is not None:
            last.replace_int("expert_option", lastnode.child_value("expert_option"))
            last.replace_int("sort", lastnode.child_value("sort"))
            last.replace_int("category", lastnode.child_value("category"))

            settings = lastnode.child("settings")
            if settings is not None:
                last.replace_int("matching", settings.child_value("matching"))
                last.replace_int("hazard", settings.child_value("hazard"))
                last.replace_int("hard", settings.child_value("hard"))
                last.replace_int("marker", settings.child_value("marker"))
                last.replace_int("theme", settings.child_value("theme"))
                last.replace_int("title", settings.child_value("title"))
                last.replace_int("parts", settings.child_value("parts"))
                last.replace_int("rank_sort", settings.child_value("rank_sort"))
                last.replace_int("combo_disp", settings.child_value("combo_disp"))
                last.replace_int_array("emblem", 5, settings.child_value("emblem"))

        # Grab unlock progress
        item = player.child("item")
        if item is not None:
            newprofile.replace_int_array("music_list", 64, item.child_value("music_list"))
            newprofile.replace_int_array("theme_list", 16, item.child_value("theme_list"))
            newprofile.replace_int_array("marker_list", 16, item.child_value("marker_list"))
            newprofile.replace_int_array("title_list", 160, item.child_value("title_list"))
            newprofile.replace_int_array("parts_list", 160, item.child_value("parts_list"))
            newprofile.replace_int_array("commu_list", 16, item.child_value("commu_list"))

            if not force_unlock:
                # Don't persist if we're force-unlocked, this data will be bogus.
                owned_songs = self.calculate_owned_items(item.child_value("secret_list"))
                for index in owned_songs:
                    self.data.local.user.put_achievement(
                        self.game,
                        self.version,
                        userid,
                        index,
                        "song",
                        {},
                    )

            owned_emblems = self.calculate_owned_items(item.child_value("emblem_list"))
            for index in owned_emblems:
                self.data.local.user.put_achievement(
                    self.game,
                    self.version,
                    userid,
                    index,
                    "emblem",
                    {},
                )

            newitem = item.child("new")
            if newitem is not None:
                newprofile.replace_int_array("theme_list_new", 16, newitem.child_value("theme_list"))
                newprofile.replace_int_array("marker_list_new", 16, newitem.child_value("marker_list"))

                if not force_unlock:
                    # Don't persist if we're force-unlocked, this data will be bogus.
                    owned_secrets = self.calculate_owned_items(newitem.child_value("secret_list"))
                    for index in owned_secrets:
                        self.data.local.user.put_achievement(
                            self.game,
                            self.version,
                            userid,
                            index,
                            "secret",
                            {},
                        )

        # Grab categories stuff
        fill_in_category = player.child("fill_in_category")
        if fill_in_category is not None:
            fill_in_category_normal = fill_in_category.child("normal")
            if fill_in_category_normal is not None:
                newprofile.replace_int_array(
                    "normal_no_gray_flag_list",
                    16,
                    fill_in_category_normal.child_value("no_gray_flag_list"),
                )
                newprofile.replace_int_array(
                    "normal_all_yellow_flag_list",
                    16,
                    fill_in_category_normal.child_value("all_yellow_flag_list"),
                )
                newprofile.replace_int_array(
                    "normal_full_combo_flag_list",
                    16,
                    fill_in_category_normal.child_value("full_combo_flag_list"),
                )
                newprofile.replace_int_array(
                    "normal_excellent_flag_list",
                    16,
                    fill_in_category_normal.child_value("excellent_flag_list"),
                )
            fill_in_category_hard = fill_in_category.child("hard")
            if fill_in_category_hard is not None:
                newprofile.replace_int_array(
                    "hard_no_gray_flag_list",
                    16,
                    fill_in_category_hard.child_value("no_gray_flag_list"),
                )
                newprofile.replace_int_array(
                    "hard_all_yellow_flag_list",
                    16,
                    fill_in_category_hard.child_value("all_yellow_flag_list"),
                )
                newprofile.replace_int_array(
                    "hard_full_combo_flag_list",
                    16,
                    fill_in_category_hard.child_value("full_combo_flag_list"),
                )
                newprofile.replace_int_array(
                    "hard_excellent_flag_list",
                    16,
                    fill_in_category_hard.child_value("excellent_flag_list"),
                )

        # jbox stuff
        jbox = player.child("jbox")
        jboxdict = newprofile.get_dict("jbox")
        if jbox is not None:
            jboxdict.replace_int("point", jbox.child_value("point"))
            emblemtype = jbox.child_value("emblem/type")
            index = jbox.child_value("emblem/index")
            if emblemtype == self.JBOX_EMBLEM_NORMAL:
                jboxdict.replace_int("normal_index", index)
            elif emblemtype == self.JBOX_EMBLEM_PREMIUM:
                jboxdict.replace_int("premium_index", index)
        newprofile.replace_dict("jbox", jboxdict)

        # event stuff
        newprofile.replace_int("event_flag", player.child_value("event_flag"))
        event_info = player.child("event_info")
        if event_info is not None:
            for child in event_info.children:
                try:
                    eventid = int(child.attribute("type"))
                except TypeError:
                    # Event is empty
                    continue
                is_completed = child.child_value("is_completed")

                # Figure out if we should update the rating/scores or not
                oldevent = self.data.local.user.get_achievement(
                    self.game,
                    self.version,
                    userid,
                    eventid,
                    "event",
                )

                if oldevent is None:
                    # Create a new event structure for this
                    oldevent = ValidatedDict()

                oldevent.replace_bool("is_completed", is_completed)

                # Save it as an achievement
                self.data.local.user.put_achievement(
                    self.game,
                    self.version,
                    userid,
                    eventid,
                    "event",
                    oldevent,
                )

        # Still don't know what this is for lol
        newprofile.replace_int("navi_flag", player.child_value("navi/flag"))

        # Grab scores and save those
        if result is not None:
            for tune in result.children:
                if tune.name != "tune":
                    continue
                result = tune.child("player")
                songid = tune.child_value("music")
                timestamp = tune.child_value("timestamp") / 1000
                chart = self.game_to_db_chart(
                    int(result.child("score").attribute("seq")),
                    bool(result.child_value("is_hard_mode")),
                )
                points = result.child_value("score")
                flags = int(result.child("score").attribute("clear"))
                combo = int(result.child("score").attribute("combo"))
                ghost = result.child_value("mbar")
                music_rate = result.child_value("music_rate")

                stats = {
                    "perfect": result.child_value("nr_perfect"),
                    "great": result.child_value("nr_great"),
                    "good": result.child_value("nr_good"),
                    "poor": result.child_value("nr_poor"),
                    "miss": result.child_value("nr_miss"),
                }

                # Miscelaneous last data for echoing to profile get
                last.replace_int("music_id", songid)
                last.replace_int("seq_id", int(result.child("score").attribute("seq")))

                mapping = {
                    self.GAME_FLAG_BIT_CLEARED: self.PLAY_MEDAL_CLEARED,
                    self.GAME_FLAG_BIT_FULL_COMBO: self.PLAY_MEDAL_FULL_COMBO,
                    self.GAME_FLAG_BIT_EXCELLENT: self.PLAY_MEDAL_EXCELLENT,
                    self.GAME_FLAG_BIT_NEARLY_FULL_COMBO: self.PLAY_MEDAL_NEARLY_FULL_COMBO,
                    self.GAME_FLAG_BIT_NEARLY_EXCELLENT: self.PLAY_MEDAL_NEARLY_EXCELLENT,
                }

                # Figure out the highest medal based on bits passed in
                medal = self.PLAY_MEDAL_FAILED
                for bit in mapping:
                    if flags & bit > 0:
                        medal = max(medal, mapping[bit])

                self.update_score(
                    userid,
                    timestamp,
                    songid,
                    chart,
                    points,
                    medal,
                    combo,
                    ghost,
                    stats,
                    music_rate,
                )

        # Born stuff
        born = player.child("born")
        if born is not None:
            newprofile.replace_int("born_status", born.child_value("status"))
            newprofile.replace_int("born_year", born.child_value("year"))

        # jubility list sent looks like this
        # <jubility>
        #     <target_music>
        #         <hot_music_list param="36385">
        #             <music>
        #                 <music_id __type="s32">90000130</music_id>
        #                 <seq __type="s8">2</seq>
        #                 <rate __type="s32">975</rate>
        #                 <value __type="s32">1280</value>
        #                 <is_hard_mode __type="bool">0</is_hard_mode>
        #             </music>
        #         </hot_music_list>
        #         <other_music_list param="36770">

        # Grab jubility
        jubility = player.child("jubility")
        if jubility is not None:
            target_music = jubility.child("target_music")

            # Pick up jubility stuff
            hot_music_list = target_music.child("hot_music_list")
            pick_up_chart = ValidatedDict()
            for music in hot_music_list.children:
                music_id = music.child_value("music_id")
                chart = self.game_to_db_chart(
                    int(music.child_value("seq")),
                    bool(music.child_value("is_hard_mode")),
                )
                music_rate = float(music.child_value("rate")) / 10
                value = float(music.child_value("value")) / 10
                entry = {
                    "music_id": music_id,
                    "seq": chart,
                    "music_rate": music_rate,
                    "value": value,
                }
                pick_up_chart.replace_dict(f"{music_id}_{chart}", entry)

            # Save it back
            newprofile.replace_dict("pick_up_chart", pick_up_chart)
            newprofile.replace_float("pick_up_jubility", float(hot_music_list.attribute("param")) / 10)

            # Common jubility stuff
            other_music_list = target_music.child("other_music_list")
            common_chart = ValidatedDict()
            for music in other_music_list.children:
                music_id = music.child_value("music_id")
                chart = self.game_to_db_chart(
                    int(music.child_value("seq")),
                    bool(music.child_value("is_hard_mode")),
                )
                music_rate = float(music.child_value("rate")) / 10
                value = float(music.child_value("value")) / 10
                entry = {
                    "music_id": music_id,
                    "seq": chart,
                    "music_rate": music_rate,
                    "value": value,
                }
                common_chart.replace_dict(f"{music_id}_{chart}", entry)

            # Save it back
            newprofile.replace_dict("common_chart", common_chart)
            newprofile.replace_float("common_jubility", float(other_music_list.attribute("param")) / 10)

        # Clan course saving
        clan_course_list = player.child("course_list")
        if clan_course_list is not None:
            for course in clan_course_list.children:
                if course.name != "course":
                    continue

                courseid = int(course.attribute("id"))
                status = course.child_value("status")
                is_seen = (status & self.COURSE_STATUS_SEEN) != 0
                is_played = (status & self.COURSE_STATUS_PLAYED) != 0

                # Update seen status and played status
                oldcourse = self.data.local.user.get_achievement(
                    self.game,
                    self.version,
                    userid,
                    courseid,
                    "course",
                )

                if oldcourse is None:
                    # Create a new course structure for this
                    oldcourse = ValidatedDict()

                oldcourse.replace_bool("seen", oldcourse.get_bool("seen") or is_seen)
                oldcourse.replace_bool("played", oldcourse.get_bool("played") or is_played)

                # Save it as an achievement
                self.data.local.user.put_achievement(
                    self.game,
                    self.version,
                    userid,
                    courseid,
                    "course",
                    oldcourse,
                )

        # If they played a course, figure out if they cleared it.
        select_course = player.child("select_course")
        if select_course is not None:
            try:
                courseid = int(select_course.attribute("id"))
            except Exception:
                courseid = 0
            cleared = select_course.child_value("is_cleared")

            if courseid > 0 and cleared:
                # Update course cleared status
                oldcourse = self.data.local.user.get_achievement(
                    self.game,
                    self.version,
                    userid,
                    courseid,
                    "course",
                )

                if oldcourse is None:
                    # Create a new course structure for this
                    oldcourse = ValidatedDict()

                oldcourse.replace_bool("cleared", True)

                # Save it as an achievement
                self.data.local.user.put_achievement(
                    self.game,
                    self.version,
                    userid,
                    courseid,
                    "course",
                    oldcourse,
                )

        # Save back last information gleaned from results
        newprofile.replace_dict("last", last)

        festo_dungeon = player.child("festo_dungeon")
        if festo_dungeon is not None:
            newprofile.replace_int("festo_dungeon_phase", festo_dungeon.child_value("phase"))
            newprofile.replace_int("festo_dungeon_clear_flag", festo_dungeon.child_value("clear_flag"))

        # Keep track of play statistics
        self.update_play_statistics(userid)

        return newprofile
