[
  {
    "path": ".editorconfig",
    "content": "# Editor configuration, see http://editorconfig.org\nroot = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\nindent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\nmax_line_length = off\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"root\": true,\n  \"ignorePatterns\": [\"**/*\"],\n  \"plugins\": [\"@nrwl/nx\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {\n        \"@nrwl/nx/enforce-module-boundaries\": [\n          \"error\",\n          {\n            \"enforceBuildableLibDependency\": true,\n            \"allow\": [],\n            \"depConstraints\": [\n              {\n                \"sourceTag\": \"*\",\n                \"onlyDependOnLibsWithTags\": [\"*\"]\n              }\n            ]\n          }\n        ]\n      }\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"extends\": [\"plugin:@nrwl/nx/typescript\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"extends\": [\"plugin:@nrwl/nx/javascript\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Often used LFS tracks\n# https://gist.github.com/ma-al/019f7f76498c55f0061120a5b13c6d88\n# ------------------------------------------------\n# Usual image types\n*.png filter=lfs diff=lfs merge=lfs -text\n*.jpeg filter=lfs diff=lfs merge=lfs -text\n*.jpg filter=lfs diff=lfs merge=lfs -text\n*.bmp filter=lfs diff=lfs merge=lfs -text\n*.svg filter=lfs diff=lfs merge=lfs -text\n*.sketch filter=lfs diff=lfs merge=lfs -text\n*.gif filter=lfs diff=lfs merge=lfs -text\n# ------------------------------------------------\n\n# Audio files\n*.ogg filter=lfs diff=lfs merge=lfs -text\n*.mp3 filter=lfs diff=lfs merge=lfs -text\n*.wav filter=lfs diff=lfs merge=lfs -text\n\n# osu! related files\n*.osr filter=lfs diff=lfs merge=lfs -text\n*.db filter=lfs diff=lfs merge=lfs -text\n*.osu filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce** (optional)\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. iOS]\n - Version [e.g. 22]\n\n**Additional context**\n\n**Please** also provide the following if they are mentioned and related:\n* Beatmap\n* Replay\n* Skin\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Additional context** (optional)\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/workflows/build-release.yml",
    "content": "name: Build/release v2\n\non:\n  push:\n  workflow_dispatch:\n\njobs:\n  release:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [ ubuntu-latest, windows-latest, macos-latest ]\n\n    steps:\n      - name: Checkout code\n        uses: nschloe/action-cached-lfs-checkout@v1\n\n      - name: Install Node.js, NPM and Yarn\n        uses: actions/setup-node@v3\n        with:\n          node-version: 20\n          cache: 'npm'\n\n      - name: Build/release Electron app\n        uses: Yan-Jobs/action-electron-builder@v1.7.0\n        with:\n          # GitHub token, automatically provided to the action\n          # (No need to define this secret in the repo settings)\n          github_token: ${{ secrets.github_token }}\n\n          # If the commit is tagged with a version (e.g. \"v1.0.0\"),\n          # release the app after building\n          release: ${{ startsWith(github.ref, 'refs/tags/v') }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# compiled output\n/dist\n/tmp\n/out-tsc\n\n# dependencies\n/node_modules\n\n# IDEs and editors\n/.idea\n.project\n.classpath\n.c9/\n*.launch\n.settings/\n*.sublime-workspace\n\n# IDE - VSCode\n.vscode/*\n!.vscode/settings.json\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# misc\n/.sass-cache\n/connect.lock\n/coverage\n/libpeerconnection.log\nnpm-debug.log\nyarn-error.log\ntestem.log\n/typings\n\n# System Files\n.DS_Store\nThumbs.db\n\n# Custom\n/logs/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"testdata/osu-testdata\"]\n\tpath = testdata/osu-testdata\n\turl = https://github.com/abstrakt8/osu-testdata\n"
  },
  {
    "path": ".prettierignore",
    "content": "# Add files here to ignore them from prettier formatting\n\n/dist\n/coverage\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"all\",\n  \"endOfLine\": \"lf\"\n}\n"
  },
  {
    "path": ".storybook/main.js",
    "content": "module.exports = {\n  stories: [],\n  addons: [\n    \"@storybook/addon-links\",\n    \"@storybook/addon-essentials\",\n    \"storybook-css-modules-preset\",\n    {\n      name: \"@storybook/addon-postcss\",\n      options: {\n        postcssLoaderOptions: {\n          implementation: require(\"postcss\"),\n        },\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".storybook/preview.js",
    "content": "export const parameters = {\n  actions: { argTypesRegex: \"^on[A-Z].*\" },\n  controls: {\n    matchers: {\n      color: /(background|color)$/i,\n      date: /Date$/,\n    },\n  },\n}"
  },
  {
    "path": ".storybook/tsconfig.json",
    "content": "{\n  \"extends\": \"../tsconfig.base.json\",\n  \"exclude\": [\n    \"../**/*.spec.js\",\n    \"../**/*.spec.ts\",\n    \"../**/*.spec.tsx\",\n    \"../**/*.spec.jsx\"\n  ],\n  \"include\": [\"../**/*\"]\n}\n"
  },
  {
    "path": ".storybook/webpack.config.js",
    "content": "/**\n * Export a function. Accept the base config as the only param.\n * @param {Object} options\n * @param {Required<import('webpack').Configuration>} options.config\n * @param {'DEVELOPMENT' | 'PRODUCTION'} options.mode - change the build configuration. 'PRODUCTION' is used when building the static version of storybook.\n */\nmodule.exports = async ({ config, mode }) => {\n  // Make whatever fine-grained changes you need\n\n  // Return the altered config\n  return config;\n};\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"nrwl.angular-console\",\n    \"esbenp.prettier-vscode\",\n    \"firsttris.vscode-jest-runner\",\n    \"dbaeumer.vscode-eslint\"\n  ]\n}\n"
  },
  {
    "path": ".yarnrc",
    "content": "# https://github.com/yarnpkg/yarn/issues/5540\n# because of material-ui icons\nnetwork-timeout 600000\n\n\n"
  },
  {
    "path": "CONTRIBUTION.md",
    "content": "Project Structure\n===\n\nGeneral\n---\n\nThis repository is a mono-repo that is currently focused on the development of the Rewind desktop application. However,\nas we are working with web technologies (WebGL, JS), there are also plans to implement a Rewind web version.\n\nThis mono-repo is powered by the [nx](https://nx.dev/) build system.\n\nIn `apps` you will find the applications - every folder corresponds to one \"product\".\n\nIn `libs` you will find the common code that is shared across all the applications.\n\nIn `tests` you will find the integration tests that will test the correctness of the libraries. Generally speaking,\ntests that have some external dependencies (such as a beatmap or replay file) will belong here. Unit tests should be\nwritten in the corresponding library `src` folder with `*.spec.ts` filename extension.\n\n> Familiarity with [Electron's process model](https://www.electronjs.org/docs/latest/tutorial/process-model) required.\n \nSetup\n===\nBasics\n---\n\nInstall the following:\n\n* Latest Node.js LTS version (e.g. `nvm install --lts`)\n* [`git-lfs`](https://git-lfs.github.com/) for the test data in `testdata/`\n\n\nWhen changes have been done to some submodules, you need to merge them as follows:\n```\ngit submodule update --remote --merge\n```\n\nBuilding\n---\n\n```bash\nyarn install\nyarn run build\n```\n\nDeveloping\n---\n\nFirst start the `desktop-frontend` to expose the frontend on port 4200 with \"Hot Reloading\" enabled.\n\n```bash\nyarn run desktop-frontend:dev\n```\n\nThen start the Electron application:\n\n```bash\nyarn run desktop-main:dev\n```\n\nIf you make a change in the `desktop-main` package, you will need to rerun the command above again.\n\n\nReleasing\n---\n\nWhen you want to create a new release, follow these steps:\n\n1. Update the version in your project's `package.json` file (e.g. `1.2.3`)\n2. Commit that change (`git commit -am v1.2.3`)\n3. Tag your commit (`git tag v1.2.3`). Make sure your tag name's format is `v*.*.*`. Your workflow will use this tag to detect when to create a release\n4. Push your changes to GitHub (`git push && git push --tags`)\n\nAfter building successfully, the action will publish your release artifacts. By default, a new release draft will be created on GitHub with download links for your app. If you want to change this behavior, have a look at the [`electron-builder` docs](https://www.electron.build).\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2021 abstrakt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">Rewind</h1>\n\n![Github releases](https://img.shields.io/github/v/release/abstrakt8/rewind?label=stable)\n![Github releases](https://img.shields.io/github/v/release/abstrakt8/rewind?include_prereleases&label=latest)\n[![GitHub Releases Downloads](https://img.shields.io/github/downloads/abstrakt8/rewind/total?label=Downloads)](https://github.com/abstrakt8/rewind/releases/latest)\n[![Discord](https://img.shields.io/discord/841454370888351784.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/QubdHdnBVg)\n\nRewind is a beatmap/replay analyzer for [osu!](https://osu.ppy.sh/) and is currently in development phase.\n\n<img src=\"resources/readme/ed_can_fc_ror.gif\" alt=\"BTMC on Road of Resistance\" /><br/>\n\n## Features\n\nThis [video](https://www.youtube.com/watch?v=KDatdxvjdmc) shows most of the features listed down below:\n\n* Music playback\n* Speed change 0.25x-4.0x\n* Scrubbing (jumping to a specific time)\n* Toggling the \"Hidden\" mod\n* Analysis cursor\n* Slider dev mode\n* Hit error bar (slightly improved)\n* Statistics such as UR\n* Watching frame by frame\n* Skin support\n* Difficulty graph on the timeline\n* Gameplay events (Miss/SB/50/100) in the timeline\n* Support for unsubmitted maps\n* File watcher\n* Customizability of many elements (e.g. cursor size)\n* Shortcuts\n* ...\n\n## Download\n\nThe latest release for Windows/Linux can be found at the [release page](https://github.com/abstrakt8/rewind/releases).\n\n## Questions / Feedback / Discussions\n\nIf you have any questions about Rewind or want to contribute by submitting ideas, feel free to join\nthe [osu! University Discord](https://discord.gg/QubdHdnBVg). It is an improvement-focused osu! hub, osu!\ncoaching hub, and a platform for experienced players to spread their game knowledge to the public.\n\n## Contribution (development, testing, documentation, ...)\n\nIf you want to contribute, please join the [Dev Discord](https://discord.gg/pwCVATunVt).\n\nCurrently, a large portion of the code is in a \"volatile\" / \"prototype\" / \"proof of concept\" state. So if you want to contribute by developing, please notify me beforehand.\n"
  },
  {
    "path": "apps/desktop/README.md",
    "content": "The Rewind desktop application consists of two renderers which are implemented in `backend` and `frontend`.\n\nThe entry point of this application is implemented in \"main.js\".\n"
  },
  {
    "path": "apps/desktop/frontend/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@nrwl/react/babel\",\n      {\n        \"runtime\": \"automatic\"\n      }\n    ]\n  ],\n  \"plugins\": []\n}\n"
  },
  {
    "path": "apps/desktop/frontend/.browserslistrc",
    "content": "# This file is used by:\n# 1. autoprefixer to adjust CSS to support the below specified browsers\n# 2. babel preset-env to adjust included polyfills\n#\n# For additional information regarding the format and rule options, please see:\n# https://github.com/browserslist/browserslist#queries\n#\n# If you need to support different browsers in production, you may tweak the list below.\n\nlast 1 Chrome version\nlast 1 Firefox version\nlast 2 Edge major versions\nlast 2 Safari major version\nlast 2 iOS major versions\nFirefox ESR\nnot IE 9-11 # For IE 9-11 support, remove 'not'."
  },
  {
    "path": "apps/desktop/frontend/.eslintrc.json",
    "content": "{\n  \"extends\": [\"plugin:@nrwl/nx/react\", \"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/frontend/.storybook/main.js",
    "content": "const rootMain = require(\"../../../../.storybook/main\");\n\n// Use the following syntax to add addons!\n// rootMain.addons.push('');\nrootMain.stories.push(...[\"../src/**/*.stories.mdx\", \"../src/**/*.stories.@(js|jsx|ts|tsx)\"]);\n\nmodule.exports = rootMain;\n"
  },
  {
    "path": "apps/desktop/frontend/.storybook/preview.js",
    "content": "import React from \"react\";\n\nimport { addDecorator } from \"@storybook/react\";\n\nimport { ThemeProvider as EmotionThemeProvider } from \"emotion-theming\";\nimport { rewindTheme } from \"../src/app/styles/theme\";\nimport { CssBaseline, ThemeProvider } from \"@mui/material\";\n\nconst defaultTheme = rewindTheme;\n\nexport const parameters = {\n  actions: { argTypesRegex: \"^on[A-Z].*\" },\n  controls: {\n    matchers: {\n      color: /(background|color)$/i,\n      date: /Date$/,\n    },\n  },\n};\n\nconst withThemeProvider = (Story, context) => {\n  return (\n    <EmotionThemeProvider theme={defaultTheme}>\n      <ThemeProvider theme={defaultTheme}>\n        <CssBaseline />\n        <Story {...context} />\n      </ThemeProvider>\n    </EmotionThemeProvider>\n  );\n};\n\naddDecorator(withThemeProvider);\n"
  },
  {
    "path": "apps/desktop/frontend/.storybook/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../../libs/feature-replay-viewer/tsconfig.json\",\n  \"compilerOptions\": {\n    \"emitDecoratorMetadata\": true,\n    \"outDir\": \"tsconfig\"\n  },\n  \"files\": [\n    \"../../node_modules/@nrwl/react/typings/cssmodule.d.ts\",\n    \"../../node_modules/@nrwl/react/typings/image.d.ts\"\n  ],\n  \"exclude\": [\n    \"../**/*.spec.ts\",\n    \"../**/*.test.ts\",\n    \"../**/*.spec.js\",\n    \"../**/*.test.js\",\n    \"../**/*.spec.tsx\",\n    \"../**/*.test.tsx\",\n    \"../**/*.spec.jsx\",\n    \"../**/*.test.jsx\"\n  ],\n  \"include\": [\n    \"../src/**/*\",\n    \"*.js\"\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/frontend/.storybook/webpack.config.js",
    "content": "const TsconfigPathsPlugin = require(\"tsconfig-paths-webpack-plugin\");\nconst rootWebpackConfig = require(\"../../../../.storybook/webpack.config\");\n/**\n * Export a function. Accept the base config as the only param.\n *\n * @param {Parameters<typeof rootWebpackConfig>[0]} options\n */\nmodule.exports = async ({ config, mode }) => {\n  config = await rootWebpackConfig({ config, mode });\n\n  const tsPaths = new TsconfigPathsPlugin({\n    configFile: \"./tsconfig.base.json\",\n  });\n\n  config.resolve.plugins ? config.resolve.plugins.push(tsPaths) : (config.resolve.plugins = [tsPaths]);\n\n  // Found this here: https://github.com/nrwl/nx/issues/2859\n  // And copied the part of the solution that made it work\n\n  const svgRuleIndex = config.module.rules.findIndex((rule) => {\n    const { test } = rule;\n\n    // @rewind: very important to have test?.\n    return test?.toString().startsWith(\"/\\\\.(svg|ico\");\n  });\n  config.module.rules[svgRuleIndex].test = /\\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\\?.*)?$/;\n\n  config.module.rules.push(\n    {\n      test: /\\.(png|jpe?g|gif|webp)$/,\n      loader: require.resolve(\"url-loader\"),\n      options: {\n        limit: 10000, // 10kB\n        name: \"[name].[hash:7].[ext]\",\n      },\n    },\n    {\n      test: /\\.svg$/,\n      oneOf: [\n        // If coming from JS/TS file, then transform into React component using SVGR.\n        {\n          issuer: {\n            test: /\\.[jt]sx?$/,\n          },\n          use: [\n            {\n              loader: require.resolve(\"@svgr/webpack\"),\n              options: {\n                svgo: false,\n                titleProp: true,\n                ref: true,\n              },\n            },\n            {\n              loader: require.resolve(\"url-loader\"),\n              options: {\n                limit: 10000, // 10kB\n                name: \"[name].[hash:7].[ext]\",\n                esModule: false,\n              },\n            },\n          ],\n        },\n        // Fallback to plain URL loader.\n        {\n          use: [\n            {\n              loader: require.resolve(\"url-loader\"),\n              options: {\n                limit: 10000, // 10kB\n                name: \"[name].[hash:7].[ext]\",\n              },\n            },\n          ],\n        },\n      ],\n    },\n  );\n\n  return config;\n};\n"
  },
  {
    "path": "apps/desktop/frontend/README.md",
    "content": "This is the frontend that gets deployed with the Electron app. That's why there is\n\nSome notes:\n\n- Uses `redux` for state management.\n- The `rewind` app is kind of a standalone application, therefore it's not really connected to the redux state\n  management.\n"
  },
  {
    "path": "apps/desktop/frontend/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"frontend\",\n  preset: \"../../../jest.preset.js\",\n  transform: {\n    \"^(?!.*\\\\.(js|jsx|ts|tsx|css|json)$)\": \"@nrwl/react/plugins/jest\",\n    \"^.+\\\\.[tj]sx?$\": \"babel-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/apps/frontend\",\n};\n"
  },
  {
    "path": "apps/desktop/frontend/proxy.conf.json",
    "content": "{\n  \"/api\": {\n    \"target\": \"http://localhost:3333\",\n    \"secure\": false\n  },\n  \"/desktop-backend-api\": {\n    \"target\": \"http://localhost:3333\",\n    \"secure\": false\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/RewindApp.tsx",
    "content": "import { useAppDispatch } from \"./hooks/redux\";\nimport { Outlet, Routes, Route, useNavigate } from \"react-router-dom\";\nimport { LeftMenuSidebar } from \"./components/sidebar/LeftMenuSidebar\";\nimport { SplashScreen } from \"./screens/splash/SplashScreen\";\nimport { HomeScreen } from \"./screens/home/HomeScreen\";\nimport { Box, Divider, Stack } from \"@mui/material\";\nimport { UpdateModal } from \"./components/update/UpdateModal\";\nimport { useEffect } from \"react\";\nimport { downloadFinished, downloadProgressed, newVersionAvailable } from \"./store/update/slice\";\nimport { frontendAPI } from \"./api\";\nimport { ipcRenderer } from \"electron\";\nimport { useTheaterContext } from \"./providers/TheaterProvider\";\nimport { ELECTRON_UPDATE_FLAG } from \"./utils/constants\";\nimport { Analyzer } from \"./screens/analyzer/Analyzer\";\nimport { SetupScreen } from \"./screens/setup/SetupScreen\";\n\nfunction NormalView() {\n  return (\n    <Stack direction={\"row\"} sx={{ height: \"100vh\" }}>\n      <LeftMenuSidebar />\n      <Divider orientation={\"vertical\"} />\n      <Box sx={{ flexGrow: 1, height: \"100%\" }}>\n        <Outlet />\n      </Box>\n      <UpdateModal />\n    </Stack>\n  );\n}\n\nexport function RewindApp() {\n  const navigate = useNavigate();\n  const dispatch = useAppDispatch();\n  const theater = useTheaterContext();\n\n  useEffect(() => {\n    void theater.common.initialize();\n\n    // For now, we will just navigate to the analyzer app since we only have one tool\n    ipcRenderer.on(\"onManualReplayOpen\", (event, file) => {\n      navigate(\"/app/analyzer\");\n      void theater.analyzer.loadReplay(file);\n    });\n\n    (async function () {\n      if (!(await theater.analyzer.osuFolderService.hasValidOsuFolderSet())) {\n        console.log(\"osu! folder was not set, redirecting to the setup screen.\");\n        navigate(\"/setup\");\n      } else {\n        console.log(`osu! folder = ${theater.analyzer.osuFolderService.getOsuFolder()}`);\n        console.log(`osu!/Songs folder = ${theater.analyzer.osuFolderService.songsFolder$.getValue()}`);\n        console.log(`osu!/Replays folder = ${theater.analyzer.osuFolderService.replaysFolder$.getValue()}`);\n        navigate(\"/app/analyzer\");\n      }\n    })();\n\n    if (ELECTRON_UPDATE_FLAG) {\n      frontendAPI.onUpdateAvailable((version) => {\n        dispatch(newVersionAvailable(version));\n      });\n      frontendAPI.onDownloadFinished(() => {\n        dispatch(downloadFinished());\n      });\n      frontendAPI.onUpdateDownloadProgress((updateInfo) => {\n        const { total, bytesPerSecond, transferred } = updateInfo;\n        dispatch(downloadProgressed({ downloadedBytes: transferred, totalBytes: total, bytesPerSecond }));\n      });\n      // We start checking for update on the front end, otherwise if we start it from the Electron main process, the\n      // notification might get lost (probably need a message queue if we want to start from the Electron main process)\n      frontendAPI.checkForUpdate();\n    }\n  }, []);\n\n  return (\n    <Routes>\n      <Route index element={<SplashScreen />} />\n      <Route path={\"setup\"} element={<SetupScreen />} />\n      <Route path={\"app\"} element={<NormalView />}>\n        <Route path={\"home\"} element={<HomeScreen />} />\n        <Route path={\"analyzer\"} element={<Analyzer />} />\n      </Route>\n    </Routes>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/api.ts",
    "content": "import { ipcRenderer } from \"electron\";\n\ntype Listener = (...args: any) => any;\n\n// Maybe use it for onUpdateDownloadProgress\ninterface UpdateInfo {\n  total: number;\n  transferred: number;\n  bytesPerSecond: number;\n}\n\nexport const frontendAPI = {\n  getPath: (type: string) => ipcRenderer.invoke(\"getPath\", type),\n  selectDirectory: (defaultPath: string) => ipcRenderer.invoke(\"selectDirectory\", defaultPath),\n  selectFile: (defaultPath: string) => ipcRenderer.invoke(\"selectFile\", defaultPath),\n  reboot: () => ipcRenderer.invoke(\"reboot\"),\n  getAppVersion: () => ipcRenderer.invoke(\"getAppVersion\"),\n  getPlatform: () => ipcRenderer.invoke(\"getPlatform\"),\n  onManualReplayOpen: (listener: Listener) => ipcRenderer.on(\"onManualReplayOpen\", (event, file) => listener(file)),\n  onUpdateAvailable: (listener: Listener) => ipcRenderer.on(\"onUpdateAvailable\", (event, version) => listener(version)),\n  onUpdateDownloadProgress: (listener: Listener) =>\n    ipcRenderer.on(\"onUpdateDownloadProgress\", (event, info) => listener(info)),\n  startDownloadingUpdate: () => ipcRenderer.invoke(\"startDownloadingUpdate\"),\n  onDownloadFinished: (listener: Listener) => ipcRenderer.on(\"onDownloadFinished\", (event) => listener()),\n  checkForUpdate: () => ipcRenderer.invoke(\"checkForUpdate\"),\n  quitAndInstall: () => ipcRenderer.invoke(\"quitAndInstall\"),\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseAudioSettingsPanel.tsx",
    "content": "import { Box, Slider, Stack, Tooltip, Typography } from \"@mui/material\";\nimport { VolumeDown, VolumeUp } from \"@mui/icons-material\";\n\ntype Change = (x: number) => void;\n\ninterface BaseAudioSettingsPanelProps {\n  master: number;\n  effects: number;\n  music: number;\n\n  onMasterChange: Change;\n  onEffectsChange: Change;\n  onMusicChange: Change;\n  onMutedChange: (muted: boolean) => unknown;\n}\n\nexport function VolumeSlider({ disabled, onChange, value }: { disabled?: boolean; onChange: Change; value: number }) {\n  return (\n    <Stack gap={2} direction={\"row\"}>\n      <VolumeDown />\n      <Slider\n        size=\"small\"\n        valueLabelDisplay=\"auto\"\n        disabled={disabled}\n        value={Math.floor(value * 100)}\n        step={1}\n        onChange={(_, x) => onChange((x as number) / 100)}\n      />\n      <VolumeUp />\n    </Stack>\n  );\n}\n\nexport function BaseAudioSettingsPanel(props: BaseAudioSettingsPanelProps) {\n  const { master, effects, music, onMasterChange, onEffectsChange, onMusicChange, onMutedChange } = props;\n  return (\n    <Stack\n      sx={{\n        p: 2,\n      }}\n      gap={1}\n    >\n      <Box>\n        <Typography id=\"input-slider\" gutterBottom>\n          Master Volume\n        </Typography>\n        <VolumeSlider onChange={onMasterChange} value={master} />\n      </Box>\n\n      <Box>\n        <Typography id=\"input-slider\" gutterBottom>\n          Music Volume\n        </Typography>\n        <VolumeSlider onChange={onMusicChange} value={music} />\n      </Box>\n      <Box>\n        <Typography id=\"input-slider\" gutterBottom>\n          Effects Volume\n        </Typography>\n        <Tooltip title={\"Hit sounds coming soon!\"}>\n          {/*https://mui.com/components/tooltips/#disabled-elements*/}\n          <span>\n            <VolumeSlider disabled={true} onChange={onEffectsChange} value={effects} />\n          </span>\n        </Tooltip>\n      </Box>\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseCurrentTime.stories.tsx",
    "content": "import { Meta, Story } from \"@storybook/react\";\nimport { Paper } from \"@mui/material\";\nimport { BaseCurrentTime, GameCurrentTimeProps } from \"./BaseCurrentTime\";\n\nexport default {\n  component: BaseCurrentTime,\n  title: \"BaseCurrentTime\",\n  argTypes: {\n    onClick: { action: \"onClick executed!\" },\n  },\n} as Meta;\n\nconst Template: Story<GameCurrentTimeProps> = (args) => (\n  <Paper elevation={1}>\n    <BaseCurrentTime {...args} />\n  </Paper>\n);\n\nexport const Time200ms = Template.bind({});\nTime200ms.args = {\n  currentTimeInMs: 200,\n};\n\nexport const Time1727ms = Template.bind({});\nTime1727ms.args = {\n  currentTimeInMs: 1727,\n};\n\nexport const TimeHour = Template.bind({});\nTimeHour.args = {\n  currentTimeInMs: 60 * 60 * 1000 + 727,\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseCurrentTime.tsx",
    "content": "import React, { forwardRef, ForwardRefRenderFunction, useImperativeHandle, useRef } from \"react\";\nimport { formatGameTime } from \"@osujs/math\";\nimport { darken, Typography } from \"@mui/material\";\n\nexport type GameCurrentTimeProps = Record<string, unknown>;\n\nexport interface GameCurrentTimeHandle {\n  updateTime: (timeInMs: number) => void;\n}\n\nconst ForwardCurrentTime: ForwardRefRenderFunction<GameCurrentTimeHandle, GameCurrentTimeProps> = (props, ref) => {\n  const refMain = useRef<HTMLSpanElement>(null);\n  const refMs = useRef<HTMLSpanElement>(null);\n\n  useImperativeHandle(ref, () => ({\n    updateTime(timeInMs) {\n      const [timeHMS, timeMS] = formatGameTime(timeInMs, true).split(\".\");\n      if (refMain.current) refMain.current.textContent = timeHMS;\n      if (refMs.current) refMs.current.textContent = \".\" + timeMS;\n    },\n  }));\n  return (\n    <Typography component={\"span\"} sx={{ userSelect: \"all\" }}>\n      <span ref={refMain}>0:00</span>\n\n      <Typography component={\"span\"} sx={{ color: (theme) => darken(theme.palette.text.primary, 0.6) }} ref={refMs}>\n        <span ref={refMs}>.000</span>\n      </Typography>\n    </Typography>\n  );\n};\n\nexport const BaseCurrentTime = forwardRef(ForwardCurrentTime);\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseDialog.tsx",
    "content": "import { IconButton, Link, Typography } from \"@mui/material\";\nimport { FaDiscord } from \"react-icons/fa\";\nimport React from \"react\";\nimport { RewindLinks } from \"../../utils/constants\";\nimport { useCommonManagers } from \"../../providers/TheaterProvider\";\n\nexport function PromotionFooter() {\n  const appVersion = useCommonManagers().appInfoService.version;\n  return (\n    <Typography fontSize={\"caption.fontSize\"} color={\"text.secondary\"}>\n      Rewind {appVersion} by{\" \"}\n      <Link href={RewindLinks.OsuPpyShAbstrakt} target={\"_blank\"} color={\"text.secondary\"}>\n        abstrakt\n      </Link>{\" \"}\n      | osu! University\n      <IconButton href={RewindLinks.OsuUniDiscord} target={\"_blank\"} size={\"small\"}>\n        <FaDiscord />\n      </IconButton>\n    </Typography>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseGameTimeSlider.stories.tsx",
    "content": "import { Box } from \"@mui/material\";\nimport { Meta, Story } from \"@storybook/react\";\nimport { BaseGameTimeSlider, BaseGameTimeSliderProps } from \"./BaseGameTimeSlider\";\n\nexport default {\n  component: BaseGameTimeSlider,\n  title: \"BaseGameTimeSlider\",\n  argTypes: {\n    onClick: { action: \"onClick executed!\" },\n  },\n} as Meta;\n\nconst Template: Story<BaseGameTimeSliderProps> = (args) => (\n  <Box width={\"420px\"}>\n    <BaseGameTimeSlider {...args} />\n  </Box>\n);\n\nexport const Primary = Template.bind({});\nPrimary.args = {\n  backgroundEnable: true,\n};\n\nexport const NotEnabled = Template.bind({});\nNotEnabled.args = {\n  backgroundEnable: false,\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseGameTimeSlider.tsx",
    "content": "import { Box, Slider, styled } from \"@mui/material\";\nimport { formatGameTime, rgbToInt } from \"@osujs/math\";\nimport { ignoreFocus } from \"../../utils/focus\";\nimport { useEffect, useRef } from \"react\";\nimport { Renderer, Sprite, Texture } from \"pixi.js\";\nimport { AdjustmentFilter } from \"@pixi/filter-adjustment\";\nimport { Container } from \"@pixi/display\";\nimport { Chart, registerables } from \"chart.js\";\nimport colorString from \"color-string\";\n\n//\nChart.register(...registerables);\n\ninterface EventLineProps {\n  color: string;\n  tooltip: string;\n  positions: number[];\n}\n\ntype EventType = { timings: number[]; tooltip: string; color: string };\n\nexport interface BaseGameTimeSliderProps {\n  backgroundEnable?: boolean;\n  // Duration in ms\n  duration: number;\n  // Time in ms\n  currentTime: number;\n  // When the slider is dragged\n  onChange: (value: number) => any;\n\n  events: EventType[];\n  difficulties: number[];\n}\n\nfunction drawPlaybarEvents(canvas: HTMLCanvasElement, eventTypes: EventType[], duration: number) {\n  const renderer = new Renderer({ view: canvas, backgroundAlpha: 0.0 });\n  const stage = new Container();\n  const { height, width } = renderer.screen;\n  const eventLineHeight = height / eventTypes.length;\n  for (let i = 0; i < eventTypes.length; i++) {\n    const eventType = eventTypes[i];\n    const container = new Container();\n    const descriptor = colorString.get(eventType.color);\n    if (!descriptor) break;\n    const tint = rgbToInt(descriptor.value);\n    // console.log(`Event ${i} has color ${tint} and ${descriptor.value}`);\n    for (const timing of eventType.timings) {\n      const sprite = Sprite.from(Texture.WHITE);\n      sprite.tint = tint;\n      // TODO: This needs to be centered -> but doesn't really matter since it's kinda negligible\n      sprite.width = 1;\n      sprite.height = eventLineHeight;\n      sprite.position.set((timing / duration) * width, 0);\n      container.addChild(sprite);\n    }\n    stage.addChild(container);\n    container.position.set(0, i * eventLineHeight);\n  }\n  stage.filters = [new AdjustmentFilter({ brightness: 0.7 })];\n\n  stage.interactive = false;\n  stage.interactiveChildren = false;\n  renderer.render(stage);\n  // Doesn't need ticker right?\n}\n\nconst PlaybarEventsCanvas = styled(\"canvas\")`\n  //background: aqua;\n  position: absolute;\n  height: 40px;\n  width: 100%;\n  top: 0;\n  left: 0;\n  transform: translate(0, -50%);\n`;\n\nconst DifficultyCanvas = styled(\"canvas\")`\n  //position: absolute;\n  //top: 0;\n  //left: 0;\n  //transform: translate(0, -100%);\n  //background: aqua;\n`;\n\nfunction drawDifficulty(canvas: HTMLCanvasElement, data: number[]) {\n  // const labels = [0, 20, 40, 50, 99, 1000];\n  const labels = data.map((_) => \"\");\n\n  const chart = new Chart(canvas, {\n    type: \"line\",\n    options: {\n      maintainAspectRatio: false,\n\n      // To hide the little \"knobs\"\n      elements: {\n        point: {\n          radius: 0,\n        },\n      },\n      scales: {\n        x: {\n          display: false,\n        },\n        y: {\n          display: false,\n        },\n      },\n      plugins: {\n        legend: {\n          display: false,\n        },\n        tooltip: {\n          enabled: false,\n        },\n      },\n    },\n    data: {\n      labels,\n      datasets: [\n        {\n          data,\n          fill: true,\n          // borderColor: \"rgba(255, 255, 255, 0.5)\",\n          // borderColor: \"hsla(0,4%,31%,0.77)\",\n          backgroundColor: \"hsla(0, 2%, 44%, 0.3)\",\n          tension: 0.5,\n        },\n      ],\n    },\n  });\n  chart.draw();\n  return chart;\n}\n\nexport function BaseGameTimeSlider(props: BaseGameTimeSliderProps) {\n  const { backgroundEnable, duration, currentTime, onChange, events, difficulties } = props;\n  const valueLabelFormat = (value: number) => formatGameTime(value);\n\n  const eventsCanvas = useRef<HTMLCanvasElement>(null!);\n  const difficultyCanvas = useRef<HTMLCanvasElement>(null!);\n\n  useEffect(() => {\n    drawPlaybarEvents(eventsCanvas.current, events, duration);\n  }, [eventsCanvas, events, duration]);\n  useEffect(() => {\n    const chart = drawDifficulty(difficultyCanvas.current, difficulties);\n    return () => {\n      chart.destroy();\n    };\n  }, [difficultyCanvas, difficulties]);\n\n  return (\n    <Box sx={{ width: \"100%\", position: \"relative\" }}>\n      <PlaybarEventsCanvas ref={eventsCanvas} />\n      <Box sx={{ position: \"absolute\", top: 0, transform: \"translate(0, -100%)\", width: \"100%\" }}>\n        <Box sx={{ position: \"relative\", height: \"48px\", width: \"100%\" }}>\n          <DifficultyCanvas ref={difficultyCanvas} />\n        </Box>\n      </Box>\n      <Slider\n        onFocus={ignoreFocus}\n        size={\"small\"}\n        // The padding here determines how clickable the slider is\n        // This is copied from: https://mui.com/components/slider/#music-player\n        sx={{\n          position: \"absolute\",\n          top: \"50%\",\n          transform: \"translate(0, -50%)\",\n          padding: \"2px 0\",\n\n          color: \"white\",\n          height: 6,\n          \"& .MuiSlider-thumb\": {\n            width: 8,\n            height: 8,\n            // transition: \"0.3s bezier(.47,1.64,.41,.8)\",\n            transition: \"none\",\n            \"&:before\": {\n              boxShadow: \"0 2px 12px 0 rgba(0,0,0,0.4)\",\n            },\n            \"&:hover, &.Mui-focusVisible\": {\n              boxShadow: `0px 0px 0px 8px ${\"rgb(255 255 255 / 16%)\"}`,\n            },\n            \"&.Mui-active\": {\n              width: 12,\n              height: 12,\n            },\n          },\n          \"& .MuiSlider-rail\": {\n            opacity: 0.28,\n          },\n          \"& .MuiSlider-track\": {\n            transition: \"none\", // Otherwise it lags behind on very short songs\n          },\n        }}\n        value={currentTime}\n        onChange={(_, x) => onChange(x as number)}\n        getAriaValueText={valueLabelFormat}\n        valueLabelFormat={valueLabelFormat}\n        valueLabelDisplay={\"auto\"}\n        step={16}\n        max={duration}\n      />\n    </Box>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseSettingsModal.stories.tsx",
    "content": "import { Meta, Story } from \"@storybook/react\";\nimport { BaseSettingsModal, SettingsProps } from \"./BaseSettingsModal\";\nimport { Paper } from \"@mui/material\";\n\nexport default {\n  component: BaseSettingsModal,\n  title: \"BaseSettingsModal\",\n  argTypes: {\n    onClose: { action: \"onClick executed!\" },\n  },\n} as Meta;\n\nconst Template: Story<SettingsProps> = (args) => (\n  <Paper elevation={2} sx={{ width: 560 }}>\n    <BaseSettingsModal {...args} />\n  </Paper>\n);\n\nexport const Primary = Template.bind({});\nPrimary.args = {\n  tabs: [\n    { component: <div>General</div>, label: \"General\" },\n    { component: <div>Skinning</div>, label: \"Cool\" },\n  ],\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/BaseSettingsModal.tsx",
    "content": "import { Box, Divider, IconButton, Paper, Slider, Stack, Tab, Tabs, Typography } from \"@mui/material\";\nimport React from \"react\";\nimport { Close, Settings as SettingsIcon, Visibility, VisibilityOff } from \"@mui/icons-material\";\nimport { PromotionFooter } from \"./BaseDialog\";\n\ninterface SettingTab {\n  component: React.ReactNode;\n  label: string;\n}\n\nexport interface SettingsProps {\n  onClose?: () => void;\n  tabs: Array<SettingTab>;\n\n  opacity: number;\n  onOpacityChange: (o: number) => unknown;\n\n  tabIndex: number;\n  onTabIndexChange: (i: number) => unknown;\n}\n\nconst MIN_OPACITY = 25;\nconst MAX_OPACITY = 100;\n\nexport function BaseSettingsModal(props: SettingsProps) {\n  const { onClose, tabs, opacity, onOpacityChange, tabIndex, onTabIndexChange } = props;\n\n  const handleTabChange = (event: any, newValue: any) => {\n    onTabIndexChange(newValue);\n  };\n\n  const displayedTab = tabs[tabIndex].component;\n\n  return (\n    <Paper\n      sx={{\n        filter: `opacity(${opacity}%)`,\n        height: \"100%\",\n        display: \"flex\",\n        flexDirection: \"column\",\n        position: \"relative\",\n      }}\n      elevation={2}\n    >\n      <Stack sx={{ py: 1, px: 2, alignItems: \"center\" }} direction={\"row\"} gap={1}>\n        <SettingsIcon />\n        <Typography fontWeight={\"bolder\"}>Settings</Typography>\n        <Box flexGrow={1} />\n        <IconButton onClick={onClose}>\n          <Close />\n        </IconButton>\n      </Stack>\n      <Divider />\n      <Stack direction={\"row\"} sx={{ flexGrow: 1, overflow: \"auto\" }}>\n        {/*TODO: Holy moly, the CSS here needs to be changed a bit*/}\n        <Tabs\n          orientation=\"vertical\"\n          variant=\"scrollable\"\n          value={tabIndex}\n          onChange={handleTabChange}\n          sx={{ borderRight: 1, borderColor: \"divider\", position: \"absolute\" }}\n        >\n          {tabs.map(({ label }, index) => (\n            <Tab label={label} key={index} tabIndex={index} sx={{ textTransform: \"none\" }} />\n          ))}\n        </Tabs>\n        {/*TODO: For example this should not (?) be hardcoded */}\n        <Box sx={{ marginLeft: \"90px\" }}>{displayedTab}</Box>\n      </Stack>\n      <Divider />\n      <Stack sx={{ px: 2, py: 1, flexDirection: \"row\", alignItems: \"center\" }}>\n        <PromotionFooter />\n        <Box flexGrow={1} />\n        <Stack direction={\"row\"} alignItems={\"center\"} gap={2}>\n          <IconButton onClick={() => onOpacityChange(MIN_OPACITY)}>\n            <VisibilityOff />\n          </IconButton>\n          <Slider\n            value={opacity}\n            onChange={(_, v) => onOpacityChange(v as number)}\n            step={5}\n            min={MIN_OPACITY}\n            max={MAX_OPACITY}\n            valueLabelFormat={(value: number) => `${value}%`}\n            sx={{ width: \"12em\" }}\n            valueLabelDisplay={\"auto\"}\n          />\n          <IconButton onClick={() => onOpacityChange(MAX_OPACITY)}>\n            <Visibility />\n          </IconButton>\n        </Stack>\n      </Stack>\n    </Paper>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/GameCanvas.tsx",
    "content": "import React, { useEffect, useRef } from \"react\";\nimport { useAnalysisApp } from \"../../providers/TheaterProvider\";\nimport { Box, CircularProgress, IconButton, Stack, Tooltip, Typography } from \"@mui/material\";\nimport { useObservable } from \"rxjs-hooks\";\nimport { LightningBoltIcon } from \"@heroicons/react/solid\";\nimport InfoIcon from \"@mui/icons-material/Info\";\nimport { ignoreFocus } from \"../../utils/focus\";\nimport CloseIcon from \"@mui/icons-material/Close\";\n\nfunction EmptyState() {\n  return (\n    <Stack gap={2} alignItems={\"center\"}>\n      <Stack gap={1} alignItems={\"center\"}>\n        <InfoIcon sx={{ height: \"2em\", width: \"2em\" }} />\n        <Typography>No replay loaded</Typography>\n      </Stack>\n      <Stack gap={1} alignItems={\"center\"} direction={\"row\"}>\n        <Box component={LightningBoltIcon} sx={{ height: \"1em\", color: \"text.secondary\" }} />\n        <Typography color={\"text.secondary\"}>\n          In osu! press F2 while being at a score/fail screen to load the replay\n        </Typography>\n      </Stack>\n      <Stack gap={1} alignItems={\"center\"} direction={\"row\"}>\n        <Box component={LightningBoltIcon} sx={{ height: \"1em\", color: \"text.secondary\" }} />\n        <Typography color={\"text.secondary\"}>\n          You can also load a replay with the menu action \"File &gt; Open Replay (Ctrl+O)\"\n        </Typography>\n      </Stack>\n    </Stack>\n  );\n}\n\nexport const GameCanvas = () => {\n  const canvas = useRef<HTMLCanvasElement | null>(null);\n  const containerRef = useRef<HTMLDivElement | null>(null);\n  const analysisApp = useAnalysisApp();\n  const { status } = useObservable(() => analysisApp.scenarioManager.scenario$, { status: \"DONE\" });\n\n  const showOverlay = status !== \"DONE\";\n\n  useEffect(() => {\n    if (containerRef.current) {\n      containerRef.current.append(analysisApp.stats());\n    }\n  }, [analysisApp]);\n  useEffect(() => {\n    if (status === \"INIT\") {\n      analysisApp.stats().hidden = true;\n    } else {\n      analysisApp.stats().hidden = false;\n    }\n  }, [status, analysisApp]);\n\n  useEffect(() => {\n    if (canvas.current) {\n      console.log(\"Initializing renderer to the canvas\");\n      analysisApp.onEnter(canvas.current);\n    }\n    return () => analysisApp.onHide();\n  }, [analysisApp]);\n\n  return (\n    <Box ref={containerRef} sx={{ borderRadius: 2, overflow: \"hidden\", position: \"relative\", flex: 1 }}>\n      {status === \"DONE\" && (\n        <Tooltip title={\"Close replay\"}>\n          <IconButton\n            sx={{ position: \"absolute\", right: \"0\", top: \"0\" }}\n            onClick={() => analysisApp.scenarioManager.clearReplay()}\n            onFocus={ignoreFocus}\n          >\n            <CloseIcon />\n          </IconButton>\n        </Tooltip>\n      )}\n      <canvas\n        style={{\n          width: \"100%\",\n          height: \"100%\",\n          // , pointerEvents: \"none\"\n        }}\n        ref={canvas}\n        // This does not work\n      />\n      {/*Overlay*/}\n      {showOverlay && (\n        <Box\n          sx={{\n            position: \"absolute\",\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            top: \"0\",\n            left: \"0\",\n            height: \"100%\",\n            width: \"100%\",\n            backgroundColor: \"rgba(0,0,0,0.7)\",\n          }}\n        >\n          {status === \"INIT\" && <EmptyState />}\n          {status === \"LOADING\" && <CircularProgress />}\n          {status === \"ERROR\" && <Typography>Something wrong happened...</Typography>}\n        </Box>\n      )}\n    </Box>\n  );\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/HelpModal.stories.tsx",
    "content": "import { Meta, Story } from \"@storybook/react\";\nimport { HelpBox } from \"./HelpModal\";\n\nexport default {\n  component: HelpBox,\n  title: \"HelpModal\",\n  argTypes: {\n    onClose: { action: \"onClose executed!\" },\n  },\n} as Meta;\n\nconst Template: Story<void> = (args) => <HelpBox onClose={() => {}} />;\n\nexport const Primary = Template.bind({});\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/HelpModal.tsx",
    "content": "import React from \"react\";\nimport styled from \"styled-components\";\nimport { Box, Divider, IconButton, Modal, Paper, Stack, Typography } from \"@mui/material\";\nimport { Close, Help } from \"@mui/icons-material\";\nimport { PromotionFooter } from \"./BaseDialog\";\n\nexport function Key({ text }: { text: string }) {\n  return (\n    <Box\n      component={\"span\"}\n      sx={{\n        backgroundColor: \"hsl(0,2%,44%)\",\n        fontFamily: \"monospace\",\n        borderStyle: \"solid\",\n        borderRadius: 1,\n        borderWidth: \"0px 4px 4px 0\",\n        // borderBottomWidth: 2,\n        // borderRightWidth: 4,\n        borderColor: \"hsl(0,2%,20%)\",\n        paddingLeft: 1,\n        paddingRight: 1,\n      }}\n    >\n      {text}\n    </Box>\n    // <span className={\"bg-gray-300 font-mono text-gray-800 rounded border-b-4 border-r-4 border-gray-600 pl-1 pr-1\"}>\n    //   {text}\n    // </span>\n  );\n}\n\nconst ShortcutBox = styled.div`\n  display: grid;\n  grid-template-columns: max-content 1fr;\n  grid-column-gap: 1em;\n  grid-row-gap: 0.5em;\n  align-items: center;\n  justify-content: center;\n`;\n\nconst leftArrowKey = \"←\";\nconst rightArrowKey = \"→\";\nconst upArrowKey = \"↑\";\nconst downArrowKey = \"↓\";\n\nconst leftKeys = [leftArrowKey, \"a\"];\nconst rightKeys = [rightArrowKey, \"d\"];\n\nfunction KeyBindings({ separator = \"+\", keys, inline }: { separator?: string; keys: string[]; inline?: boolean }) {\n  return (\n    <div className={inline ? \"inline\" : \"\"}>\n      {keys.map((k, i) => (\n        <React.Fragment key={i}>\n          <Key text={k} />\n          {i + 1 < keys.length && ` ${separator} `}\n        </React.Fragment>\n      ))}\n    </div>\n  );\n}\n\nfunction Title({ children }: any) {\n  return <Typography variant={\"h6\"}>{children}</Typography>;\n}\n\n// Which one?\nconst orSeparator = \" or \";\n\n// const orSeparator = \" , \";\n\nfunction PlaybarNavigationShortcuts() {\n  return (\n    <Stack sx={{ py: 2 }} flexDirection={\"column\"} gap={1}>\n      <Title>Shortcuts</Title>\n      <ShortcutBox>\n        <KeyBindings keys={[\"Spacebar\"]} /> <span>Start / Pause</span>\n        <KeyBindings separator={orSeparator} keys={[upArrowKey, \"w\"]} /> <span>Increase speed</span>\n        <KeyBindings separator={orSeparator} keys={[downArrowKey, \"s\"]} /> <span>Decrease speed</span>\n        <KeyBindings separator={orSeparator} keys={leftKeys} /> <span>Small jump back</span>\n        <KeyBindings separator={orSeparator} keys={rightKeys} /> <span>Small jump forward</span>\n        {/*<div>*/}\n        {/*  <KeyBindings keys={[\"Ctrl\"]} inline /> + <KeyBindings separator={orSeparator} keys={leftKeys} inline />*/}\n        {/*</div>*/}\n        <KeyBindings separator={\"+\"} keys={[\"Ctrl\", leftArrowKey]} />\n        <span>Micro jump back</span>\n        {/*<div>*/}\n        {/*  <KeyBindings keys={[\"Ctrl\"]} inline /> + <KeyBindings separator={orSeparator} keys={rightKeys} inline />*/}\n        {/*</div>*/}\n        <KeyBindings separator={\"+\"} keys={[\"Ctrl\", rightArrowKey]} />\n        <span>Micro jump forward</span>\n        {/*<div>*/}\n        {/*  <KeyBindings keys={[\"Shift\"]} inline /> + <KeyBindings separator={orSeparator} keys={leftKeys} inline />*/}\n        {/*</div>*/}\n        <KeyBindings separator={\"+\"} keys={[\"Shift\", leftArrowKey]} />\n        <span>Large jump back</span>\n        {/*<div>*/}\n        {/*  <KeyBindings keys={[\"Shift\"]} inline /> + <KeyBindings separator={orSeparator} keys={rightKeys} inline />*/}\n        {/*</div>*/}\n        <KeyBindings separator={\"+\"} keys={[\"Shift\", rightArrowKey]} />\n        <span>Large jump forward</span>\n        <KeyBindings keys={[\"f\"]} /> <span>Toggle hidden</span>\n      </ShortcutBox>\n    </Stack>\n  );\n}\n\ninterface HelpModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport function HelpBox(props: Pick<HelpModalProps, \"onClose\">) {\n  const { onClose } = props;\n  return (\n    <Paper sx={{ px: 2, py: 2, display: \"flex\", flexDirection: \"column\" }}>\n      {/*MenuBar could be reused*/}\n      <Stack sx={{ alignItems: \"center\" }} direction={\"row\"} gap={1}>\n        <Help />\n        <Typography fontWeight={\"bolder\"}>Help</Typography>\n        <Box flexGrow={1} />\n        <IconButton onClick={onClose}>\n          <Close />\n        </IconButton>\n      </Stack>\n      <Divider />\n\n      <PlaybarNavigationShortcuts />\n      {/*<OtherResources />*/}\n      {/*Footer*/}\n      <Divider />\n      <Stack sx={{ paddingTop: 1 }}>\n        <PromotionFooter />\n      </Stack>\n    </Paper>\n  );\n}\n\nexport function HelpModalDialog(props: HelpModalProps) {\n  const { isOpen, onClose } = props;\n\n  return (\n    <Modal open={isOpen} onClose={onClose}>\n      <Box\n        sx={{\n          position: \"absolute\",\n          top: \"50%\",\n          left: \"50%\",\n          transform: \"translate(-50%, -50%)\",\n          minWidth: 600,\n          // maxWidth: 700,\n          maxWidth: \"100%\",\n          // maxHeight: 600,\n          maxHeight: \"100%\",\n        }}\n      >\n        <HelpBox onClose={onClose} />\n      </Box>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/PlayBar.tsx",
    "content": "import {\n  Box,\n  Button,\n  Divider,\n  IconButton,\n  ListItemIcon,\n  ListItemText,\n  Menu,\n  MenuItem,\n  MenuList,\n  Popover,\n  Stack,\n  Tooltip,\n  Typography,\n} from \"@mui/material\";\nimport {\n  Help,\n  MoreVert,\n  PauseCircle,\n  PhotoCamera,\n  PlayCircle,\n  Settings,\n  VolumeOff,\n  VolumeUp,\n} from \"@mui/icons-material\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { BaseAudioSettingsPanel } from \"./BaseAudioSettingsPanel\";\nimport { BaseGameTimeSlider } from \"./BaseGameTimeSlider\";\nimport { useGameClockControls, useGameClockTime } from \"../../hooks/game-clock\";\nimport { formatGameTime } from \"@osujs/math\";\nimport { useAudioSettings, useAudioSettingsService } from \"../../hooks/audio\";\nimport { useModControls } from \"../../hooks/mods\";\nimport modHiddenImg from \"../../../assets/mod_hidden.png\";\nimport { ALLOWED_SPEEDS, PlaybarColors } from \"../../utils/constants\";\n\nimport { useSettingsModalContext } from \"../../providers/SettingsProvider\";\nimport { ReplayAnalysisEvent } from \"@osujs/core\";\nimport { useObservable } from \"rxjs-hooks\";\nimport FiberManualRecordIcon from \"@mui/icons-material/FiberManualRecord\";\nimport { HelpModalDialog } from \"./HelpModal\";\nimport { BaseCurrentTime, GameCurrentTimeHandle } from \"./BaseCurrentTime\";\nimport { ignoreFocus } from \"../../utils/focus\";\nimport { useAnalysisApp, useCommonManagers } from \"../../providers/TheaterProvider\";\nimport { DEFAULT_PLAY_BAR_SETTINGS } from \"../../services/common/playbar\";\n\nconst centerUp = {\n  anchorOrigin: {\n    vertical: \"top\",\n    horizontal: \"center\",\n  },\n  transformOrigin: {\n    vertical: \"bottom\",\n    horizontal: \"center\",\n  },\n};\n\nfunction MoreMenu() {\n  const [anchorEl, setAnchorEl] = useState(null);\n  const open = Boolean(anchorEl);\n  const handleClick = (event: any) => {\n    setAnchorEl(event.currentTarget);\n  };\n  const handleClose = () => {\n    setAnchorEl(null);\n  };\n\n  const analyzer = useAnalysisApp();\n  const handleTakeScreenshot = () => {\n    analyzer.screenshotTaker.takeScreenshot();\n    handleClose();\n  };\n\n  const [helpOpen, setHelpOpen] = useState(false);\n\n  const handleOpenHelp = () => {\n    setHelpOpen(true);\n    handleClose();\n  };\n\n  return (\n    <>\n      <HelpModalDialog isOpen={helpOpen} onClose={() => setHelpOpen(false)} />\n      <IconButton\n        aria-label=\"more\"\n        id=\"long-button\"\n        aria-controls=\"long-menu\"\n        // aria-expanded={open ? \"true\" : undefined}\n        aria-haspopup=\"true\"\n        onClick={handleClick}\n        onFocus={ignoreFocus}\n      >\n        <MoreVert />\n      </IconButton>\n      <Menu\n        open={open}\n        onClose={handleClose}\n        anchorEl={anchorEl}\n        anchorOrigin={{\n          vertical: \"top\",\n          horizontal: \"center\",\n        }}\n        transformOrigin={{\n          vertical: \"bottom\",\n          horizontal: \"center\",\n        }}\n      >\n        <MenuItem onClick={handleTakeScreenshot}>\n          <ListItemIcon>\n            <PhotoCamera />\n          </ListItemIcon>\n          <ListItemText>Take Screenshot</ListItemText>\n        </MenuItem>\n        <MenuItem onClick={handleOpenHelp}>\n          <ListItemIcon>\n            <Help />\n          </ListItemIcon>\n          <ListItemText>Help</ListItemText>\n        </MenuItem>\n      </Menu>\n    </>\n  );\n}\n\nfunction AudioButton() {\n  const [anchorEl, setAnchorEl] = useState(null);\n  const open = Boolean(anchorEl);\n  const handlePopOverOpen = (event: any) => {\n    setAnchorEl(event.currentTarget);\n  };\n  const handleClose = () => {\n    setAnchorEl(null);\n  };\n  const { volume, muted } = useAudioSettings();\n  const service = useAudioSettingsService();\n\n  const handleClick = () => {\n    service.toggleMuted();\n  };\n  return (\n    <>\n      <IconButton onClick={handlePopOverOpen}>{muted ? <VolumeOff /> : <VolumeUp />}</IconButton>\n      <Popover\n        open={open}\n        anchorEl={anchorEl}\n        onClose={handleClose}\n        anchorOrigin={{\n          vertical: \"top\",\n          horizontal: \"center\",\n        }}\n        transformOrigin={{\n          vertical: \"bottom\",\n          horizontal: \"center\",\n        }}\n      >\n        <Box width={256}>\n          <BaseAudioSettingsPanel\n            master={volume.master}\n            music={volume.music}\n            /* Currently it's disabled */\n            effects={0}\n            onMutedChange={(x) => service.setMuted(x)}\n            onMasterChange={(x) => service.setMasterVolume(x)}\n            onMusicChange={(x) => service.setMusicVolume(x)}\n            onEffectsChange={(x) => service.setEffectsVolume(x)}\n          />\n        </Box>\n      </Popover>\n    </>\n  );\n}\n\n// Connected\nfunction PlayButton() {\n  const { isPlaying, toggleClock } = useGameClockControls();\n  const Icon = !isPlaying ? PlayCircle : PauseCircle;\n\n  return (\n    <IconButton onClick={toggleClock} onFocus={ignoreFocus}>\n      <Icon fontSize={\"large\"} />\n    </IconButton>\n  );\n}\n\n// https://css-tricks.com/using-requestanimationframe-with-react-hooks/\nconst timeAnimateFPS = 30;\n\nfunction CurrentTime() {\n  const analyzer = useAnalysisApp();\n  const requestRef = useRef<number>(0);\n  const timeRef = useRef<GameCurrentTimeHandle>(null);\n\n  // const animate = () => {};\n\n  useEffect(() => {\n    // requestRef.current = requestAnimationFrame(animate);\n    let last = -1;\n    const requiredElapsed = 1000 / timeAnimateFPS;\n\n    function animate(currentTimestamp: number) {\n      const elapsed = currentTimestamp - last;\n      if (elapsed > requiredElapsed) {\n        if (timeRef.current) timeRef.current.updateTime(analyzer.gameClock.timeElapsedInMs);\n        last = currentTimestamp;\n      }\n      requestRef.current = requestAnimationFrame(animate);\n    }\n\n    requestRef.current = requestAnimationFrame(animate);\n    return () => {\n      if (requestRef.current) cancelAnimationFrame(requestRef.current);\n    };\n  }, [analyzer]);\n  return (\n    // We MUST fix the width because the font is not monospace e.g. \"111\" is thinner than \"000\"\n    // Also if the duration is more than an hour there will also be a slight shift\n    <Box sx={{ width: \"7em\" }}>\n      <BaseCurrentTime ref={timeRef} />\n    </Box>\n  );\n}\n\nfunction groupTimings(events: ReplayAnalysisEvent[]) {\n  const missTimings: number[] = [];\n  const mehTimings: number[] = [];\n  const okTimings: number[] = [];\n  const sliderBreakTimings: number[] = [];\n\n  events.forEach((e) => {\n    switch (e.type) {\n      case \"HitObjectJudgement\":\n        // TODO: for lazer style, this needs some rework\n        if (e.isSliderHead) {\n          if (e.verdict === \"MISS\") sliderBreakTimings.push(e.time);\n          return;\n        } else {\n          if (e.verdict === \"MISS\") missTimings.push(e.time);\n          if (e.verdict === \"MEH\") mehTimings.push(e.time);\n          if (e.verdict === \"OK\") okTimings.push(e.time);\n        }\n        // if(e.verdict === \"GREAT\" && show300s) events.push(); // Not sure if this will ever be implemented\n        break;\n      case \"CheckpointJudgement\":\n        if (!e.hit && !e.isLastTick) sliderBreakTimings.push(e.time);\n        break;\n      case \"UnnecessaryClick\":\n        // TODO\n        break;\n    }\n  });\n  return { missTimings, mehTimings, okTimings, sliderBreakTimings };\n}\n\nfunction GameTimeSlider() {\n  // TODO: Depending on if replay is loaded and settings\n  const backgroundEnable = true;\n  const currentTime = useGameClockTime(15);\n  const { seekTo, duration } = useGameClockControls();\n  const { gameSimulator } = useAnalysisApp();\n  const { playbarSettingsStore } = useCommonManagers();\n  const replayEvents = useObservable(() => gameSimulator.replayEvents$, []);\n  const difficulties = useObservable(() => gameSimulator.difficulties$, []);\n  const playbarSettings = useObservable(() => playbarSettingsStore.settings$, DEFAULT_PLAY_BAR_SETTINGS);\n\n  const events = useMemo(() => {\n    const { sliderBreakTimings, missTimings, mehTimings, okTimings } = groupTimings(replayEvents);\n    return [\n      { color: PlaybarColors.MISS, timings: missTimings, tooltip: \"Misses\" },\n      { color: PlaybarColors.SLIDER_BREAK, timings: sliderBreakTimings, tooltip: \"Sliderbreaks\" },\n      { color: PlaybarColors.MEH, timings: mehTimings, tooltip: \"50s\" },\n      { color: PlaybarColors.OK, timings: okTimings, tooltip: \"100s\" },\n    ];\n  }, [replayEvents]);\n\n  return (\n    <BaseGameTimeSlider\n      backgroundEnable={backgroundEnable}\n      duration={duration}\n      currentTime={currentTime}\n      onChange={seekTo}\n      events={events}\n      difficulties={playbarSettings.difficultyGraphEnabled ? difficulties : []}\n    />\n  );\n}\n\nfunction Duration() {\n  const { duration } = useGameClockControls();\n  const f = formatGameTime(duration);\n\n  return <Typography>{f}</Typography>;\n}\n\nfunction HiddenButton() {\n  const { setHidden, hidden: hiddenEnabled } = useModControls();\n  const handleClick = useCallback(() => setHidden(!hiddenEnabled), [hiddenEnabled, setHidden]);\n\n  return (\n    <Button onFocus={ignoreFocus} onClick={handleClick} sx={{ px: 0 }}>\n      <img\n        src={modHiddenImg}\n        alt={\"ModHidden\"}\n        style={{ filter: `grayscale(${hiddenEnabled ? \"0%\" : \"100%\"})`, width: \"60%\" }}\n      />\n    </Button>\n  );\n}\n\nfunction SettingsButton() {\n  const { onSettingsModalOpenChange } = useSettingsModalContext();\n  return (\n    <IconButton onClick={() => onSettingsModalOpenChange(true)} onFocus={ignoreFocus}>\n      <Settings />\n    </IconButton>\n  );\n}\n\ninterface BaseSpeedButtonProps {\n  value: number;\n  onChange: (value: number) => any;\n}\n\nconst speedLabels: Record<number, string> = { 0.75: \"HT\", 1.5: \"DT\" } as const;\n\nfunction BaseSpeedButton(props: BaseSpeedButtonProps) {\n  const { value, onChange } = props;\n\n  const [anchorEl, setAnchorEl] = useState(null);\n  const open = Boolean(anchorEl);\n  const handleClick = (event: any) => {\n    setAnchorEl(event.currentTarget);\n  };\n  const handleClose = () => {\n    setAnchorEl(null);\n  };\n  const formatSpeed = (s: number) => `${s}x`;\n\n  // Floating point issues?\n\n  return (\n    <>\n      <Button\n        sx={{\n          color: \"text.primary\",\n          textTransform: \"none\",\n          fontSize: \"1em\",\n          // minWidth: \"0\",\n          // px: 2,\n        }}\n        size={\"small\"}\n        onClick={handleClick}\n        onFocus={ignoreFocus}\n      >\n        {formatSpeed(value)}\n        {/*<Typography>{formatSpeed(value)}</Typography>*/}\n      </Button>\n      <Menu\n        open={open}\n        onClose={handleClose}\n        anchorEl={anchorEl}\n        anchorOrigin={{\n          vertical: \"top\",\n          horizontal: \"center\",\n        }}\n        transformOrigin={{\n          vertical: \"bottom\",\n          horizontal: \"center\",\n        }}\n      >\n        <MenuList>\n          {ALLOWED_SPEEDS.map((s) => (\n            <MenuItem\n              key={s}\n              onClick={() => {\n                onChange(s);\n                handleClose();\n              }}\n              sx={{ width: \"120px\", maxWidth: \"100%\" }}\n            >\n              <ListItemText>{formatSpeed(s)}</ListItemText>\n              <Typography variant=\"body2\" color=\"text.secondary\">\n                {speedLabels[s] ?? \"\"}\n              </Typography>\n            </MenuItem>\n          ))}\n        </MenuList>\n      </Menu>\n    </>\n  );\n}\n\nfunction SpeedButton() {\n  const { speed, setSpeed } = useGameClockControls();\n  return (\n    // <Box sx={{ display: \"flex\", justifyContent: \"center\" }}>\n    <BaseSpeedButton value={speed} onChange={setSpeed} />\n    // </Box>\n  );\n}\n\nfunction RecordButton() {\n  // TODO: Probably stop at a certain time otherwise the program might crash due to memory issue\n  const { clipRecorder } = useAnalysisApp();\n  const recordingSince = useObservable(() => clipRecorder.recordingSince$, 0);\n\n  const isRecording = recordingSince > 0;\n\n  const recordingTime = \"3:00\";\n\n  const handleClick = useCallback(() => {\n    if (isRecording) {\n      clipRecorder.stopRecording();\n    } else {\n      clipRecorder.startRecording();\n    }\n  }, [isRecording, clipRecorder]);\n\n  return (\n    <Tooltip title={\"Start recording a clip\"}>\n      <IconButton onClick={handleClick}>\n        <FiberManualRecordIcon\n          sx={{\n            color: isRecording ? \"red\" : \"text.primary\",\n          }}\n        />\n      </IconButton>\n    </Tooltip>\n  );\n}\n\nconst VerticalDivider = () => <Divider orientation={\"vertical\"} sx={{ height: \"80%\" }} />;\n\nexport function PlayBar() {\n  return (\n    <Stack height={64} gap={1} p={2} direction={\"row\"} alignItems={\"center\"}>\n      <PlayButton />\n      <CurrentTime />\n      <GameTimeSlider />\n      <Duration />\n      <VerticalDivider />\n      <Stack direction={\"row\"} alignItems={\"center\"} justifyContent={\"center\"}>\n        <AudioButton />\n        <SpeedButton />\n        <HiddenButton />\n        {/*<RecordButton />*/}\n        <SettingsButton />\n        <MoreMenu />\n      </Stack>\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/analyzer/SettingsModal.tsx",
    "content": "import { useSettingsModalContext } from \"../../providers/SettingsProvider\";\nimport {\n  Autocomplete,\n  Box,\n  Button,\n  FormControlLabel,\n  FormGroup,\n  Modal,\n  Paper,\n  Slider,\n  Stack,\n  Switch,\n  TextField,\n  Typography,\n} from \"@mui/material\";\nimport { BaseSettingsModal } from \"./BaseSettingsModal\";\nimport { useCommonManagers } from \"../../providers/TheaterProvider\";\nimport { useCallback, useEffect, useMemo } from \"react\";\nimport { useObservable } from \"rxjs-hooks\";\nimport { DEFAULT_HIT_ERROR_BAR_SETTINGS } from \"../../services/common/hit-error-bar\";\nimport { DEFAULT_PLAY_BAR_SETTINGS } from \"../../services/common/playbar\";\nimport { DEFAULT_OSU_SKIN_ID, DEFAULT_REWIND_SKIN_ID, SkinId, SkinSource, stringToSkinId } from \"../../model/SkinId\";\nimport { DEFAULT_BEATMAP_RENDER_SETTINGS } from \"../../services/common/beatmap-render\";\nimport { DEFAULT_SKIN_SETTINGS } from \"../../services/common/skin\";\nimport { DEFAULT_REPLAY_CURSOR_SETTINGS } from \"../../services/common/replay-cursor\";\nimport { DEFAULT_ANALYSIS_CURSOR_SETTINGS } from \"../../services/analysis/analysis-cursor\";\nimport { frontendAPI } from \"../../api\";\n\nconst sourceName: Record<SkinSource, string> = {\n  osu: \"osu!/Skins Folder\",\n  rewind: \"Rewind\",\n};\n\nconst formatToPercent = (value: number) => `${value}%`;\n\nfunction BeatmapBackgroundSettings() {\n  const theater = useCommonManagers();\n  const { beatmapBackgroundSettingsStore } = theater;\n  const settings = useObservable(() => beatmapBackgroundSettingsStore.settings$, { blur: 0, enabled: false, dim: 0 });\n  return (\n    <Paper sx={{ boxShadow: \"none\", p: 2 }}>\n      <Stack gap={1}>\n        <Typography variant={\"h6\"}>Beatmap Background</Typography>\n        <Stack>\n          <Typography>Blur</Typography>\n          <Slider\n            value={Math.round(settings.blur * 100)}\n            onChange={(_, v) => beatmapBackgroundSettingsStore.setBlur((v as number) / 100)}\n            valueLabelDisplay={\"auto\"}\n            valueLabelFormat={formatToPercent}\n          />\n          <Typography>Dim</Typography>\n          <Slider\n            value={Math.round(settings.dim * 100)}\n            onChange={(_, v) => beatmapBackgroundSettingsStore.setDim((v as number) / 100)}\n            valueLabelDisplay={\"auto\"}\n            valueLabelFormat={formatToPercent}\n          />\n        </Stack>\n      </Stack>\n    </Paper>\n  );\n}\n\nfunction BeatmapRenderSettings() {\n  const { beatmapRenderSettingsStore } = useCommonManagers();\n  const settings = useObservable(() => beatmapRenderSettingsStore.settings$, DEFAULT_BEATMAP_RENDER_SETTINGS);\n\n  return (\n    <Stack>\n      <FormGroup>\n        <FormControlLabel\n          control={\n            <Switch\n              checked={settings.sliderDevMode}\n              onChange={(event) => beatmapRenderSettingsStore.setSliderDevMode(event.target.checked)}\n            />\n          }\n          label={\"Slider Dev Mode\"}\n        />\n      </FormGroup>\n      {/*  draw slider ends*/}\n    </Stack>\n  );\n}\n\nfunction AnalysisCursorSettingsSection() {\n  const { analysisCursorSettingsStore } = useCommonManagers();\n  const settings = useObservable(() => analysisCursorSettingsStore.settings$, DEFAULT_ANALYSIS_CURSOR_SETTINGS);\n\n  return (\n    <Paper sx={{ boxShadow: \"none\", p: 2 }}>\n      <Stack gap={1}>\n        <Typography variant={\"h6\"}>Analysis Cursor</Typography>\n        <FormGroup>\n          <FormControlLabel\n            control={\n              <Switch\n                checked={settings.enabled}\n                onChange={(event) => analysisCursorSettingsStore.setEnabled(event.target.checked)}\n              />\n            }\n            label={\"Enabled\"}\n          />\n        </FormGroup>\n      </Stack>\n    </Paper>\n  );\n}\n\nfunction ReplayCursorSettingsSection() {\n  const { replayCursorSettingsStore } = useCommonManagers();\n  const settings = useObservable(() => replayCursorSettingsStore.settings$, DEFAULT_REPLAY_CURSOR_SETTINGS);\n\n  return (\n    <Paper sx={{ boxShadow: \"none\", p: 2 }}>\n      <Stack gap={1}>\n        <Typography variant={\"h6\"}>Replay Cursor</Typography>\n        <FormGroup>\n          <FormControlLabel\n            control={\n              <Switch\n                checked={settings.enabled}\n                onChange={(event) =>\n                  replayCursorSettingsStore.changeSettings((s) => (s.enabled = event.target.checked))\n                }\n              />\n            }\n            label={\"Enabled\"}\n          />\n        </FormGroup>\n        <FormGroup>\n          <FormControlLabel\n            disabled={!settings.enabled}\n            control={\n              <Switch\n                checked={settings.smoothCursorTrail}\n                onChange={(event) =>\n                  replayCursorSettingsStore.changeSettings((s) => (s.smoothCursorTrail = event.target.checked))\n                }\n              />\n            }\n            label={\"Smooth Cursor Trail\"}\n          />\n        </FormGroup>\n        <Typography>Scale</Typography>\n        <Slider\n          value={Math.round(settings.scale * 100)}\n          valueLabelFormat={formatToPercent}\n          disabled={!settings.enabled}\n          min={10}\n          max={200}\n          onChange={(_, v) => replayCursorSettingsStore.changeSettings((s) => (s.scale = (v as number) / 100))}\n          valueLabelDisplay={\"auto\"}\n        />\n      </Stack>\n    </Paper>\n  );\n}\n\nfunction HitErrorBarSettingsSection() {\n  const { hitErrorBarSettingsStore } = useCommonManagers();\n  const settings = useObservable(() => hitErrorBarSettingsStore.settings$, DEFAULT_HIT_ERROR_BAR_SETTINGS);\n  return (\n    <Paper elevation={1} sx={{ boxShadow: \"none\", p: 2 }}>\n      <Stack>\n        {/*TODO: Enabled*/}\n        <Typography>Hit Error Bar Scaling</Typography>\n        <Slider\n          value={Math.round(settings.scale * 100)}\n          valueLabelFormat={formatToPercent}\n          max={300}\n          onChange={(_, v) => hitErrorBarSettingsStore.changeSettings((s) => (s.scale = (v as number) / 100))}\n          valueLabelDisplay={\"auto\"}\n        />\n      </Stack>\n    </Paper>\n  );\n}\n\nfunction PlaybarSettingsSection() {\n  const { playbarSettingsStore } = useCommonManagers();\n  const settings = useObservable(() => playbarSettingsStore.settings$, DEFAULT_PLAY_BAR_SETTINGS);\n\n  return (\n    <Paper elevation={1} sx={{ boxShadow: \"none\", p: 2 }}>\n      <Stack gap={1}>\n        <Typography variant={\"h6\"}>Playbar</Typography>\n        <FormGroup>\n          <FormControlLabel\n            control={\n              <Switch\n                checked={settings.difficultyGraphEnabled}\n                onChange={(event) =>\n                  playbarSettingsStore.changeSettings((s) => (s.difficultyGraphEnabled = event.target.checked))\n                }\n              />\n            }\n            label={\"Show difficulty graph\"}\n          />\n        </FormGroup>\n      </Stack>\n    </Paper>\n  );\n}\n\nfunction ResetAllSettingsSection() {\n  const resetSettings = useCallback(() => {\n    localStorage.clear();\n    frontendAPI.reboot();\n  }, []);\n  return (\n    <Button variant={\"contained\"} onClick={resetSettings}>\n      Reset All Settings and Restart\n    </Button>\n  );\n}\n\nfunction OtherSettings() {\n  return (\n    <Stack p={2} gap={1}>\n      <ResetAllSettingsSection />\n    </Stack>\n  );\n}\n\nfunction GameplaySettings() {\n  return (\n    <Stack p={2} gap={1}>\n      <ReplayCursorSettingsSection />\n      <AnalysisCursorSettingsSection />\n      <HitErrorBarSettingsSection />\n      <BeatmapBackgroundSettings />\n      <PlaybarSettingsSection />\n      <BeatmapRenderSettings />\n    </Stack>\n  );\n}\n\nfunction SkinsSettings() {\n  // TODO: Button for synchronizing skin list again\n\n  const theater = useCommonManagers();\n\n  const { preferredSkinId } = useObservable(() => theater.skinSettingsStore.settings$, DEFAULT_SKIN_SETTINGS);\n  const chosenSkinId = stringToSkinId(preferredSkinId);\n  const skins = useObservable(() => theater.skinManager.skinList$, []);\n\n  const skinOptions: SkinId[] = useMemo(\n    () => [DEFAULT_OSU_SKIN_ID, DEFAULT_REWIND_SKIN_ID].concat(skins.map((name) => ({ source: \"osu\", name }))),\n    [skins],\n  );\n\n  useEffect(() => {\n    theater.skinManager.loadSkinList();\n  }, [theater]);\n\n  // TODO:\n\n  const handleSkinChange = useCallback(\n    (skinId: SkinId) => {\n      (async function () {\n        try {\n          await theater.skinManager.loadSkin(skinId);\n        } catch (e) {\n          // Show some error dialog\n          console.error(`Could not load skin ${skinId}`);\n        }\n      })();\n      // TODO: Error handling\n    },\n    [theater],\n  );\n\n  return (\n    <Box sx={{ p: 2 }}>\n      <Autocomplete\n        id=\"skin-selection-demo\"\n        // TODO: Make sure skinIds are sorted\n        options={skinOptions}\n        groupBy={(option: SkinId) => sourceName[option.source]}\n        value={chosenSkinId}\n        onChange={(event, newValue) => {\n          if (newValue) {\n            handleSkinChange(newValue as SkinId);\n          }\n        }}\n        getOptionLabel={(option: SkinId) => option.name}\n        sx={{ width: 300 }}\n        renderInput={(params) => <TextField {...params} label=\"Skin\" />}\n        isOptionEqualToValue={(option, value) => option.name === value.name && option.source === value.source}\n      />\n    </Box>\n  );\n}\n\nexport function SettingsModal() {\n  const { onSettingsModalOpenChange, settingsModalOpen, opacity, onTabIndexChange, onOpacityChange, tabIndex } =\n    useSettingsModalContext();\n  const onClose = () => onSettingsModalOpenChange(false);\n\n  return (\n    <Modal\n      open={settingsModalOpen}\n      onClose={onClose}\n      hideBackdrop={false}\n      sx={{\n        // We reduce the default backdrop from 0.5 alpha to 0.1 in order for the user to better see the background\n        // behind the modal\n        \"& .MuiBackdrop-root\": {\n          backgroundColor: \"rgba(0,0,0,0.1)\",\n        },\n      }}\n    >\n      <Box\n        sx={{\n          position: \"absolute\",\n          top: \"50%\",\n          left: \"50%\",\n          transform: \"translate(-50%, -50%)\",\n          width: 800,\n          maxWidth: \"100%\",\n          height: 600,\n          maxHeight: \"100%\",\n        }}\n      >\n        <BaseSettingsModal\n          opacity={opacity}\n          tabIndex={tabIndex}\n          onTabIndexChange={onTabIndexChange}\n          onOpacityChange={onOpacityChange}\n          onClose={onClose}\n          tabs={[\n            { label: \"Game\", component: <GameplaySettings /> },\n            {\n              label: \"Skins\",\n              component: <SkinsSettings />,\n            },\n            { label: \"Other\", component: <OtherSettings /> },\n          ]}\n        />\n      </Box>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/logo/RewindLogo.tsx",
    "content": "import { Stack, Typography } from \"@mui/material\";\nimport { FastRewind } from \"@mui/icons-material\";\n\nexport const RewindLogo = () => (\n  <Stack alignItems={\"center\"}>\n    <FastRewind fontSize={\"large\"} />\n    <Typography fontSize={\"8px\"} sx={{ userSelect: \"none\" }}>\n      REWIND\n    </Typography>\n  </Stack>\n);\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/sidebar/LeftMenuSidebar.tsx",
    "content": "import { RewindLogo } from \"../logo/RewindLogo\";\nimport { Badge, Box, Divider, IconButton, Stack, Tooltip } from \"@mui/material\";\nimport { Home } from \"@mui/icons-material\";\nimport { FaMicroscope } from \"react-icons/fa\";\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport { useAppDispatch, useAppSelector } from \"../../hooks/redux\";\nimport UpdateIcon from \"@mui/icons-material/Update\";\nimport { setUpdateModalOpen } from \"../../store/update/slice\";\nimport { Link, useLocation, useNavigate } from \"react-router-dom\";\nimport { ELECTRON_UPDATE_FLAG } from \"../../utils/constants\";\nimport { useAppInfo } from \"../../hooks/app-info\";\n\nconst tooltipPosition = {\n  anchorOrigin: {\n    vertical: \"center\",\n    horizontal: \"right\",\n  },\n  transformOrigin: {\n    vertical: \"center\",\n    horizontal: \"left\",\n  },\n};\n\nconst repoOwner = \"abstrakt8\";\nconst repoName = \"rewind\";\n\nconst latestReleaseUrl = `https://github.com/${repoOwner}/${repoName}/releases/latest`;\nconst latestReleaseApi = `https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`;\n\nfunction useCheckForUpdate() {\n  const { appVersion } = useAppInfo();\n  const { newVersion } = useAppSelector((state) => state.updater);\n  const [state, setState] = useState<{ hasNewUpdate: boolean; latestVersion: string }>({\n    hasNewUpdate: false,\n    latestVersion: \"\",\n  });\n  useEffect(() => {\n    (async function () {\n      const response = await fetch(latestReleaseApi);\n      const json = await response.json();\n\n      // Should be something like \"v0.1.0\"\n      const tagName = json[\"tag_name\"] as string;\n      if (!tagName) {\n        return;\n      }\n      // Removes the \"v\" prefix\n      const latestVersion = tagName.substring(1);\n      const hasNewUpdate = appVersion !== latestVersion;\n      setState({ hasNewUpdate, latestVersion });\n      console.log(\n        `Current release: ${appVersion} and latest release: ${latestVersion}, therefore hasNewUpdate=${hasNewUpdate}`,\n      );\n    })();\n  }, [appVersion]);\n\n  if (ELECTRON_UPDATE_FLAG) {\n    return { hasNewUpdate: newVersion !== appVersion, latestVersion: newVersion };\n  } else {\n    return state;\n  }\n}\n\nexport function LeftMenuSidebar() {\n  const dispatch = useAppDispatch();\n  const location = useLocation();\n  const navigate = useNavigate();\n\n  const state = useCheckForUpdate();\n\n  const openUpdateModal = () => dispatch(setUpdateModalOpen(true));\n\n  const onNewUpdateAvailableButtonClick = useCallback(() => {\n    if (ELECTRON_UPDATE_FLAG) {\n      openUpdateModal();\n    } else {\n      window.open(latestReleaseUrl);\n    }\n  }, []);\n  const handleLinkClick = (to: string) => () => navigate(to);\n  const buttonColor = (name: string) => (location.pathname.endsWith(name) ? \"primary\" : \"default\");\n\n  return (\n    <Stack\n      sx={{\n        width: (theme) => theme.spacing(10),\n        paddingBottom: 2,\n      }}\n      gap={1}\n      p={1}\n      alignItems={\"center\"}\n      component={\"nav\"}\n    >\n      <Box onClick={handleLinkClick(\"home\")} sx={{ cursor: \"pointer\" }}>\n        <RewindLogo />\n      </Box>\n      <Divider orientation={\"horizontal\"} sx={{ borderWidth: 1, width: \"80%\" }} />\n      <Tooltip title={\"Overview\"} placement={\"right\"}>\n        <Link to={\"home\"}>\n          <IconButton color={buttonColor(\"/home\")}>\n            <Home />\n          </IconButton>\n        </Link>\n      </Tooltip>\n      <Tooltip title={\"Analyzer\"} placement={\"right\"}>\n        <Link to={\"analyzer\"}>\n          <IconButton\n            // These are not centered\n            color={buttonColor(\"/analyzer\")}\n          >\n            <FaMicroscope height={\"0.75em\"} />\n          </IconButton>\n        </Link>\n      </Tooltip>\n      {/*Nothing*/}\n      <Box flexGrow={1} />\n      <Tooltip\n        title={state.hasNewUpdate ? `New version ${state.latestVersion} available!` : `You are on the latest version`}\n        placement={\"right\"}\n      >\n        <IconButton onClick={onNewUpdateAvailableButtonClick}>\n          <Badge variant={\"dot\"} color={\"error\"} invisible={!state.hasNewUpdate}>\n            <UpdateIcon />\n          </Badge>\n        </IconButton>\n      </Tooltip>\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/components/update/UpdateModal.tsx",
    "content": "import { Box, Button, Divider, IconButton, LinearProgress, Link, Modal, Paper, Stack, Typography } from \"@mui/material\";\nimport { useAppDispatch, useAppSelector } from \"../../hooks/redux\";\nimport { setUpdateModalOpen } from \"../../store/update/slice\";\nimport { Close } from \"@mui/icons-material\";\nimport React from \"react\";\nimport { frontendAPI } from \"../../api\";\nimport { useAppInfo } from \"../../hooks/app-info\";\n\nconst units = [\"bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\n\nfunction niceBytes(x: any) {\n  let l = 0,\n    n = parseInt(x, 10) || 0;\n  while (n >= 1024 && ++l) {\n    n = n / 1024;\n  }\n  return n.toFixed(n < 10 && l > 0 ? 1 : 0) + \" \" + units[l];\n}\n\nfunction versionUrl(version: string) {\n  const repoOwner = \"abstrakt8\";\n  const repoName = \"rewind\";\n  // The version does not contain \"v\"\n  return `https://github.com/${repoOwner}/${repoName}/releases/v${version}`;\n}\n\nexport function UpdateModal() {\n  const { modalOpen, newVersion, isDownloading, downloadedBytes, bytesPerSecond, downloadFinished, error, totalBytes } =\n    useAppSelector((state) => state.updater);\n  const dispatch = useAppDispatch();\n  const handleClose = () => dispatch(setUpdateModalOpen(false));\n  const { appVersion } = useAppInfo();\n\n  const updateAvailable = newVersion !== null;\n  const title = updateAvailable ? `Update available` : `Up to date`;\n\n  const downloadProgress = (downloadedBytes / totalBytes) * 100;\n\n  return (\n    <Modal open={modalOpen} onClose={handleClose}>\n      <Box\n        sx={{\n          position: \"absolute\",\n          top: \"50%\",\n          left: \"50%\",\n          transform: \"translate(-50%, -50%)\",\n          maxWidth: \"100%\",\n          maxHeight: \"100%\",\n        }}\n      >\n        <Paper elevation={1}>\n          <Stack sx={{ px: 2, py: 1, alignItems: \"center\" }} direction={\"row\"} gap={1}>\n            <Typography fontWeight={\"bolder\"}>{title}</Typography>\n            <Box flexGrow={1} />\n            <IconButton onClick={handleClose}>\n              <Close />\n            </IconButton>\n          </Stack>\n          <Divider />\n          <Stack sx={{ px: 3, py: 2 }} direction={\"row\"} alignItems={\"center\"} gap={4}>\n            {!updateAvailable && <Typography>You are on the latest version: {appVersion}</Typography>}\n            {updateAvailable && (\n              <Stack gap={2}>\n                <Typography>\n                  New Rewind version available:{\" \"}\n                  <Link href={versionUrl(newVersion)} target={\"_blank\"} color={\"text.secondary\"}>\n                    {newVersion}\n                  </Link>\n                </Typography>\n                {isDownloading && (\n                  <Typography>\n                    {downloadFinished ? \"Finished downloading!\" : \"Downloading...\"}{\" \"}\n                    <Typography variant={\"caption\"}>\n                      {`${niceBytes(downloadedBytes)} / ${niceBytes(totalBytes)} (${downloadProgress.toFixed(2)} %)`}\n                    </Typography>\n                  </Typography>\n                )}\n                {isDownloading && <LinearProgress variant=\"determinate\" value={downloadProgress} />}\n                {downloadFinished && (\n                  <Typography variant={\"caption\"}>Installation will happen when you close the application.</Typography>\n                )}\n                <Stack direction={\"row-reverse\"}>\n                  {!downloadFinished && (\n                    <Button\n                      variant={\"contained\"}\n                      onClick={() => frontendAPI.startDownloadingUpdate()}\n                      disabled={isDownloading}\n                    >\n                      Download update\n                    </Button>\n                  )}\n                  {downloadFinished && (\n                    <Button variant={\"contained\"} onClick={() => frontendAPI.quitAndInstall()}>\n                      Restart and install\n                    </Button>\n                  )}\n                </Stack>\n              </Stack>\n            )}\n          </Stack>\n        </Paper>\n      </Box>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/app-info.ts",
    "content": "import { useCommonManagers } from \"../providers/TheaterProvider\";\n\n\nexport function useAppInfo() {\n  const { appInfoService } = useCommonManagers();\n\n  return {\n    appVersion: appInfoService.version,\n    platform: appInfoService.platform,\n  };\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/audio.ts",
    "content": "import { useCommonManagers } from \"../providers/TheaterProvider\";\nimport { useObservable } from \"rxjs-hooks\";\nimport { AudioSettings } from \"../services/common/audio/settings\";\n\nexport function useAudioSettingsService() {\n  const theater = useCommonManagers();\n  return theater.audioSettingsService;\n}\n\nconst defaultSettings: AudioSettings = { volume: { effects: 0, master: 0, music: 0 }, muted: true };\n\nexport function useAudioSettings() {\n  const audioSettingsService = useAudioSettingsService();\n  return useObservable(() => audioSettingsService.settings$, defaultSettings);\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/energy-saver.ts",
    "content": "// import { useStageContext } from \"../components/StageProvider/StageProvider\";\n\n// export function useEnergySaver(enabled = true) {\n//   const isVisible = usePageVisibility();\n//   // const { stage } = useStageContext();\n//   const { pauseClock } = useGameClockContext();\n//\n//   useEffect(() => {\n//     if (!enabled) {\n//       return;\n//     }\n//     if (!isVisible) {\n//       pauseClock();\n//       stage.stopTicker();\n//     } else {\n//       stage.startTicker();\n//     }\n//   }, [isVisible, stage, pauseClock, enabled]);\n// }\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/game-clock.ts",
    "content": "import { useCallback, useState } from \"react\";\nimport { useObservable } from \"rxjs-hooks\";\nimport { useAnalysisApp } from \"../providers/TheaterProvider\";\nimport { ALLOWED_SPEEDS } from \"../utils/constants\";\nimport { useInterval } from \"./interval\";\n\nexport function useGameClock() {\n  const analyzer = useAnalysisApp();\n  return analyzer.gameClock;\n}\n\n// TODO: FLOATING POINT EQUALITY ALERT\nconst speedIndex = (speed: number) => ALLOWED_SPEEDS.indexOf(speed);\nconst nextSpeed = (speed: number) => ALLOWED_SPEEDS[Math.min(ALLOWED_SPEEDS.length - 1, speedIndex(speed) + 1)];\nconst prevSpeed = (speed: number) => ALLOWED_SPEEDS[Math.max(0, speedIndex(speed) - 1)];\n\nexport function useGameClockControls() {\n  const clock = useGameClock();\n\n  const isPlaying = useObservable(() => clock.isPlaying$, false);\n  const duration = useObservable(() => clock.durationInMs$, 0);\n  const speed = useObservable(() => clock.speed$, 1.0);\n\n  const toggleClock = useCallback(() => clock.toggle(), [clock]);\n  const seekTo = useCallback((timeInMs: number) => clock.seekTo(timeInMs), [clock]);\n\n  const setSpeed = useCallback((x: number) => clock.setSpeed(x), [clock]);\n  const increaseSpeed = useCallback(() => clock.setSpeed(nextSpeed(clock.speed)), [clock]);\n  const decreaseSpeed = useCallback(() => clock.setSpeed(prevSpeed(clock.speed)), [clock]);\n\n  const seekForward = useCallback((timeInMs: number) => clock.seekTo(clock.timeElapsedInMs + timeInMs), [clock]);\n  const seekBackward = useCallback((timeInMs: number) => clock.seekTo(clock.timeElapsedInMs - timeInMs), [clock]);\n\n  return {\n    isPlaying,\n    duration,\n    speed,\n    // Actions\n    setSpeed,\n    increaseSpeed,\n    decreaseSpeed,\n    toggleClock,\n    seekTo,\n    seekForward,\n    seekBackward,\n  };\n}\n\n// 60FPS by default\nexport function useGameClockTime(fps = 60) {\n  const gameClock = useGameClock();\n  const [time, setTime] = useState(0);\n  useInterval(() => {\n    setTime(gameClock.timeElapsedInMs);\n  }, 1000 / fps);\n  return time;\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/interval.ts",
    "content": "import { useEffect, useRef } from \"react\";\n\nexport function useInterval(callback: () => void, delay: number | null) {\n  const savedCallback = useRef(callback);\n\n  // Remember the latest callback if it changes.\n  useEffect(() => {\n    savedCallback.current = callback;\n  }, [callback]);\n\n  // Set up the interval.\n  useEffect(() => {\n    // Don't schedule if no delay is specified.\n    if (delay === null) {\n      // eslint-disable-next-line @typescript-eslint/no-empty-function\n      return () => {};\n    }\n\n    const id = setInterval(() => savedCallback.current(), delay);\n\n    return () => clearInterval(id);\n  }, [delay]);\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/mods.ts",
    "content": "import { useAnalysisApp } from \"../providers/TheaterProvider\";\nimport { useObservable } from \"rxjs-hooks\";\nimport { useCallback } from \"react\";\n\nexport function useModControls() {\n  const { modSettingsService } = useAnalysisApp();\n\n  const modSettings = useObservable(() => modSettingsService.modSettings$, { flashlight: false, hidden: false });\n  const setHidden = useCallback((value: boolean) => modSettingsService.setHidden(value), [modSettingsService]);\n\n  return { ...modSettings, setHidden };\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/redux.ts",
    "content": "import { TypedUseSelectorHook, useDispatch, useSelector } from \"react-redux\";\nimport { AppDispatch, RootState } from \"../store\";\n\nexport const useAppDispatch = () => useDispatch<AppDispatch>(); // Export a hook that can be reused to resolve types\nexport const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/hooks/shortcuts.ts",
    "content": "import { useHotkeys } from \"react-hotkeys-hook\";\nimport { useGameClockControls } from \"./game-clock\";\nimport { useModControls } from \"./mods\";\n\n// TODO: Configurable\n\n// These should stay constant or make them dynamic depending on gameClock speed\nconst microscopeJump = 1;\nconst frameJump = 16; // Assuming 16fps\nconst mediumJump = 1 * 1000;\nconst largeJump = 15 * 1000;\n\nconst leftKeys = [\"a\", \"left\"];\nconst rightKeys = [\"d\", \"right\"];\n\nconst upKeys = [\"w\", \"up\"];\nconst downKeys = [\"s\", \"down\"];\n\nconst generateKeyComboSimple = (keys: string[]) => keys.join(\", \");\nconst generateKeyCombo = (modifier = \"\", keys: string[]) => keys.map((k) => `${modifier}+${k}`).join(\", \");\n\n// TODO: Get platform from AppInfo, so that we can also support MacOS (currently they are hardcoded for Windows/Linux)\nexport function useShortcuts() {\n  const { toggleClock, seekBackward, seekForward, increaseSpeed, decreaseSpeed } = useGameClockControls();\n  const { setHidden, hidden } = useModControls();\n\n  useHotkeys(generateKeyComboSimple(upKeys), () => increaseSpeed(), [increaseSpeed]);\n  useHotkeys(generateKeyComboSimple(downKeys), () => decreaseSpeed(), [decreaseSpeed]);\n  useHotkeys(\"space\", () => toggleClock(), [toggleClock]);\n\n  useHotkeys(generateKeyCombo(\"shift\", leftKeys), () => seekBackward(mediumJump), [seekBackward]);\n  useHotkeys(generateKeyCombo(\"shift\", rightKeys), () => seekForward(mediumJump), [seekForward]);\n  useHotkeys(generateKeyCombo(\"ctrl\", leftKeys), () => seekBackward(microscopeJump), [seekBackward]);\n  useHotkeys(generateKeyCombo(\"ctrl\", rightKeys), () => seekForward(microscopeJump), [seekForward]);\n  useHotkeys(generateKeyComboSimple(leftKeys), () => seekBackward(frameJump), [seekBackward]);\n  useHotkeys(generateKeyComboSimple(rightKeys), () => seekForward(frameJump), [seekForward]);\n\n  // These have really bad collisions\n  // useHotkeys(`alt+${leftKey}`, () => seekBackward(frameJump), [seekBackward]);\n  // useHotkeys(`alt+${rightKey}`, () => seekForward(frameJump), [seekForward]);\n  // useHotkeys(`ctrl+${leftKey}`, () => seekBackward(largeJump), [seekBackward]);\n  // useHotkeys(`ctrl+${rightKey}`, () => seekForward(largeJump), [seekForward]);\n\n  useHotkeys(\"f\", () => setHidden(!hidden), [hidden, setHidden]);\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/model/BlueprintInfo.ts",
    "content": "export interface BlueprintInfo {\n  md5Hash: string;\n  lastPlayed: Date;\n  title: string;\n  artist: string;\n  creator: string;\n  // This is assuming that they are in the folderName\n  folderName: string;\n  audioFileName: string;\n  osuFileName: string;\n\n  // [Events]\n  bgFileName?: string; // Usually unknown unless .osu file is parsed\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/model/OsuReplay.ts",
    "content": "// TODO: Rename this to replay or something\nimport { OsuClassicMod, ReplayFrame } from \"@osujs/core\";\n\nexport type OsuReplay = {\n  md5hash: string;\n  beatmapMd5: string;\n  gameVersion: number;\n  mods: OsuClassicMod[];\n  player: string; // Could be useful to draw\n  frames: ReplayFrame[];\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/model/Skin.ts",
    "content": "import { rgbToInt } from \"@osujs/math\";\nimport { Texture } from \"@pixi/core\";\nimport {\n  comboDigitFonts,\n  defaultDigitFonts,\n  generateDefaultSkinConfig,\n  hitCircleDigitFonts,\n  OsuSkinTextures,\n  SkinConfig,\n} from \"@rewind/osu/skin\";\n\nexport type SkinTexturesByKey = Partial<Record<OsuSkinTextures, Texture[]>>;\n\n// Read\n// https://github.com/pixijs/pixi.js/blob/dev/packages/loaders/src/TextureLoader.ts\nexport interface ISkin {\n  config: SkinConfig;\n\n  getComboColorForIndex(i: number): number;\n\n  getTexture(key: OsuSkinTextures): Texture;\n\n  getTextures(key: OsuSkinTextures): Texture[];\n\n  getHitCircleNumberTextures(): Texture[];\n\n  getComboNumberTextures(): Texture[];\n}\n\nexport class EmptySkin implements ISkin {\n  config = generateDefaultSkinConfig(false);\n\n  getComboColorForIndex(): number {\n    return 0;\n  }\n\n  getComboNumberTextures(): [] {\n    return [];\n  }\n\n  getHitCircleNumberTextures(): Texture[] {\n    return [];\n  }\n\n  getTexture() {\n    return Texture.EMPTY;\n  }\n\n  getTextures(): Texture[] {\n    return [Texture.EMPTY];\n  }\n}\n\n/**\n * A simple skin that can provide the basic information a beatmap needs.\n */\nexport class Skin implements ISkin {\n  static EMPTY = new Skin(generateDefaultSkinConfig(false), {});\n\n  constructor(public readonly config: SkinConfig, public readonly textures: SkinTexturesByKey) {}\n\n  getComboColorForIndex(i: number): number {\n    const comboColors = this.config.colors.comboColors;\n    return rgbToInt(comboColors[i % comboColors.length]);\n  }\n\n  getTexture(key: OsuSkinTextures): Texture {\n    return this.getTextures(key)[0];\n  }\n\n  getTextures(key: OsuSkinTextures): Texture[] {\n    if (!(key in this.textures)) {\n      return [Texture.EMPTY];\n      // throw new Error(\"Texture key not found\");\n    }\n    const list = this.textures[key] as Texture[];\n    if (list.length === 0) {\n      return [Texture.EMPTY];\n    } else {\n      return list;\n    }\n  }\n\n  getHitCircleNumberTextures(): Texture[] {\n    return hitCircleDigitFonts.map((h) => this.getTexture(h));\n  }\n\n  getComboNumberTextures(): Texture[] {\n    return comboDigitFonts.map((h) => this.getTexture(h));\n  }\n\n  // The textures that are used for every other numbers on the interface (except combo)\n  getScoreTextures(): Texture[] {\n    return defaultDigitFonts.map((h) => this.getTexture(h));\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/model/SkinId.ts",
    "content": "export type SkinSource = \"rewind\" | \"osu\";\n\nexport interface SkinId {\n  source: SkinSource;\n  name: string;\n}\n\nexport function skinIdToString({ source, name }: SkinId) {\n  return `${source}:${name}`;\n}\n\nconst isSkinSource = (s: string): s is SkinSource => s === \"rewind\" || s === \"osu\";\n\nexport function stringToSkinId(str: string): SkinId {\n  const [source, name] = str.split(\":\");\n  if (isSkinSource(source)) {\n    return { source, name };\n  } else {\n    throw Error(\"Skin source wrong\");\n  }\n}\n\nexport const DEFAULT_OSU_SKIN_ID: SkinId = { source: \"rewind\", name: \"OsuDefaultSkin\" };\nexport const DEFAULT_REWIND_SKIN_ID: SkinId = { source: \"rewind\", name: \"RewindDefaultSkin\" };\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/providers/SettingsProvider.tsx",
    "content": "import React, { createContext, useContext, useState } from \"react\";\n\ntype ISettingsContext = {\n  settingsModalOpen: boolean;\n  onSettingsModalOpenChange: (open: boolean) => unknown;\n\n  opacity: number;\n  onOpacityChange: (opacity: number) => unknown;\n\n  tabIndex: number;\n  onTabIndexChange: (index: number) => unknown;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-non-null-assertion\nexport const SettingsContext = createContext<ISettingsContext>(null!);\n\ninterface SettingsModalProps {\n  children: React.ReactNode;\n  defaultOpen?: boolean;\n}\n\nconst DEFAULT_OPACITY = 100; // maxOpacity = 100%\n\nexport function SettingsModalProvider({ children, defaultOpen = false }: SettingsModalProps) {\n  const [settingsModalOpen, setSettingsModalOpen] = useState(defaultOpen);\n  const [opacity, onOpacityChange] = useState(DEFAULT_OPACITY);\n  const [tabIndex, onTabIndexChange] = useState(0);\n\n  return (\n    <SettingsContext.Provider\n      value={{\n        settingsModalOpen,\n        onSettingsModalOpenChange: (b) => setSettingsModalOpen(b),\n        opacity,\n        onOpacityChange,\n        tabIndex,\n        onTabIndexChange,\n      }}\n    >\n      {children}\n    </SettingsContext.Provider>\n  );\n}\n\nexport function useSettingsModalContext() {\n  const context = useContext(SettingsContext);\n  if (!context) {\n    throw Error(\"useSettingsModalContext can only be used within a SettingsModalProvider\");\n  }\n  return context;\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/providers/TheaterProvider.tsx",
    "content": "import React, { createContext, useContext } from \"react\";\nimport { RewindTheater } from \"../services/common/CommonManagers\";\n\n// eslint-disable-next-line @typescript-eslint/no-non-null-assertion\nexport const TheaterContext = createContext<RewindTheater>(null!);\n\ninterface TheaterProviderProps {\n  theater: RewindTheater;\n  children: React.ReactNode;\n}\n\nexport function TheaterProvider({ theater, children }: TheaterProviderProps) {\n  return <TheaterContext.Provider value={theater}>{children}</TheaterContext.Provider>;\n}\n\nexport function useTheaterContext() {\n  const context = useContext(TheaterContext);\n  if (!context) {\n    throw Error(\"useTheaterContext can only be used within a TheaterProvider\");\n  }\n  return context;\n}\n\nexport function useCommonManagers() {\n  const theater = useTheaterContext();\n  return theater.common;\n}\n\nexport function useAnalysisApp() {\n  const theater = useTheaterContext();\n  return theater.analyzer;\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/screens/analyzer/Analyzer.tsx",
    "content": "import { Paper, Stack } from \"@mui/material\";\nimport { PlayBar } from \"../../components/analyzer/PlayBar\";\nimport { useShortcuts } from \"../../hooks/shortcuts\";\nimport { GameCanvas } from \"../../components/analyzer/GameCanvas\";\nimport { SettingsModal } from \"../../components/analyzer/SettingsModal\";\nimport { SettingsModalProvider } from \"../../providers/SettingsProvider\";\nimport { useEffect } from \"react\";\nimport { environment } from \"../../../environments/environment\";\nimport { useAnalysisApp } from \"../../providers/TheaterProvider\";\n\nexport function Analyzer() {\n  const analyzer = useAnalysisApp();\n  // Shortcuts will then only be available when this page is <Analyzer/> is open\n  useShortcuts();\n\n  useEffect(() => {\n    void analyzer.initialize();\n    if (environment.debugAnalyzer) {\n      void analyzer.loadReplay(environment.debugAnalyzer.replayPath);\n    }\n  }, [analyzer]);\n\n  return (\n    <SettingsModalProvider>\n      <SettingsModal />\n      <Stack\n        sx={{\n          p: 2,\n          flexGrow: 1,\n          height: \"100%\",\n        }}\n        gap={2}\n      >\n        <GameCanvas />\n        <Paper elevation={1} sx={{ boxShadow: \"none\" }}>\n          <PlayBar />\n        </Paper>\n      </Stack>\n    </SettingsModalProvider>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/screens/home/HomeScreen.tsx",
    "content": "import React from \"react\";\nimport { FaDiscord, FaTwitter, FaYoutube } from \"react-icons/fa\";\nimport { IconButton, Link, Stack, Typography } from \"@mui/material\";\nimport { FastRewind } from \"@mui/icons-material\";\nimport { useAppInfo } from \"../../hooks/app-info\";\nimport { discordUrl, RewindLinks, twitterUrl, youtubeUrl } from \"../../utils/constants\";\n\n// This page is actually just a placeholder for an overview page that can show things such as \"Recently Played\", etc.\n\nexport function HomeScreen() {\n  const { appVersion } = useAppInfo();\n  return (\n    <Stack gap={4} sx={{ justifyContent: \"center\", alignItems: \"center\", margin: \"auto\", height: \"100%\" }}>\n      <Stack alignItems={\"center\"}>\n        <FastRewind sx={{ height: \"2em\", width: \"2em\" }} />\n        <Typography fontSize={\"1em\"} sx={{ userSelect: \"none\", marginBottom: 2 }}>\n          REWIND\n        </Typography>\n        <Typography fontSize={\"caption.fontSize\"} color={\"text.secondary\"}>\n          Rewind {appVersion} by{\" \"}\n          <Link href={RewindLinks.OsuPpyShAbstrakt} target={\"_blank\"} color={\"text.secondary\"}>\n            abstrakt\n          </Link>\n        </Typography>\n        <Typography fontSize={\"caption.fontSize\"} color={\"text.secondary\"}>\n          osu! University\n          <IconButton href={discordUrl} target={\"_blank\"} size={\"small\"}>\n            <FaDiscord />\n          </IconButton>\n          <IconButton href={twitterUrl} target={\"_blank\"} size={\"small\"}>\n            <FaTwitter />\n          </IconButton>\n          <IconButton href={youtubeUrl} target={\"_blank\"} size={\"small\"}>\n            <FaYoutube />\n          </IconButton>\n        </Typography>\n      </Stack>\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/screens/setup/SetupScreen.tsx",
    "content": "import * as React from \"react\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Alert, Badge, Box, Button, IconButton, InputBase, Paper, Stack } from \"@mui/material\";\nimport { RewindLogo } from \"../../components/logo/RewindLogo\";\nimport { Help, RocketLaunch } from \"@mui/icons-material\";\nimport FolderIcon from \"@mui/icons-material/Folder\";\nimport { frontendAPI } from \"../../api\";\nimport { useNavigate } from \"react-router-dom\";\nimport { useAnalysisApp } from \"../../providers/TheaterProvider\";\n\ninterface DirectorySelectionProps {\n  value: string | null;\n  onChange: (value: string | null) => void;\n  placeHolder: string;\n  badgeOnEmpty?: boolean;\n}\n\nfunction DirectorySelection({ value, onChange, placeHolder, badgeOnEmpty }: DirectorySelectionProps) {\n  const handleSelectFolderClick = useCallback(() => {\n    frontendAPI.selectDirectory(value ?? \"\").then((path) => {\n      if (path !== null) {\n        onChange(path);\n      }\n    });\n  }, [onChange, value]);\n\n  const onInputChange = useCallback(\n    (event: any) => {\n      onChange(event.target.value);\n    },\n    [onChange],\n  );\n\n  const invisibleBadge = !badgeOnEmpty || !!value;\n  return (\n    <Paper sx={{ px: 2, py: 1, display: \"flex\", alignItems: \"center\", width: 400 }} elevation={2}>\n      {/*<span className={\"text-gray-400 select-none w-96\"}>{value ?? placeHolder}</span>*/}\n      <InputBase\n        sx={{ flex: 1 }}\n        placeholder={placeHolder}\n        value={value ?? \"\"}\n        onChange={onInputChange}\n        disabled={true}\n      />\n      <IconButton onClick={handleSelectFolderClick}>\n        <Badge invisible={invisibleBadge} color={\"primary\"} variant={\"dot\"}>\n          <FolderIcon />\n        </Badge>\n      </IconButton>\n    </Paper>\n  );\n}\n\nconst setupWikiUrl = \"https://github.com/abstrakt8/rewind/wiki/Setup\";\n\n// TODO: Maybe tell which file is actually missing\nexport function SetupScreen() {\n  // TODO: Add a guess for directory path\n  const [directoryPath, setDirectoryPath] = useState<string | null>(null);\n  const [saveEnabled, setSaveEnabled] = useState(false);\n  const navigate = useNavigate();\n  const analyzer = useAnalysisApp();\n  // const [updateOsuDirectory, updateState] = useUpdateOsuDirectoryMutation();\n  const [showErrorMessage, setShowErrorMessage] = useState(false);\n\n  const handleConfirmClick = useCallback(async () => {\n    if (!directoryPath) {\n      return;\n    }\n    const isValid = await analyzer.osuFolderService.isValidOsuFolder(directoryPath);\n    if (isValid) {\n      analyzer.osuFolderService.setOsuFolder(directoryPath);\n      navigate(\"/app/analyzer\");\n    } else {\n      setShowErrorMessage(true);\n    }\n  }, [navigate, analyzer.osuFolderService, directoryPath]);\n\n  const handleOnDirectoryChange = useCallback(\n    (path: string | null) => {\n      setDirectoryPath(path);\n      // TODO: Just directly validate since it's so fast\n      setShowErrorMessage(false);\n    },\n    [setShowErrorMessage],\n  );\n\n  // Makes sure that the button is only clickable when it's allowed.\n  useEffect(() => {\n    setSaveEnabled(directoryPath !== null);\n  }, [directoryPath]);\n\n  return (\n    <Box\n      sx={{\n        height: \"100vh\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <Paper elevation={1}>\n        <Stack gap={2} sx={{ px: 6, py: 4 }}>\n          <RewindLogo />\n          {showErrorMessage && (\n            <>\n              <Alert severity=\"error\" variant=\"filled\">\n                <div>Does not look a valid osu! directory!</div>\n              </Alert>\n            </>\n          )}\n          <DirectorySelection\n            value={directoryPath}\n            onChange={handleOnDirectoryChange}\n            placeHolder={\"Select your osu! directory\"}\n            badgeOnEmpty={true}\n          />\n          <Stack direction={\"row-reverse\"} gap={2}>\n            <Button\n              variant={\"contained\"}\n              startIcon={<RocketLaunch />}\n              disabled={!saveEnabled}\n              onClick={handleConfirmClick}\n            >\n              Save & Continue\n            </Button>\n            <Button variant={\"text\"} onClick={() => window.open(setupWikiUrl)} startIcon={<Help />}>\n              Help\n            </Button>\n          </Stack>\n        </Stack>\n      </Paper>\n    </Box>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/screens/splash/SplashScreen.tsx",
    "content": "import { HashLoader } from \"react-spinners\";\nimport { Stack } from \"@mui/material\";\n\nexport function SplashScreen() {\n  return (\n    <Stack\n      sx={{\n        height: \"100vh\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n      gap={2}\n    >\n      <HashLoader color={\"white\"} loading={true} />\n      <div>Services are getting ready ...</div>\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/AnalysisApp.ts",
    "content": "import { ReplayService } from \"../common/local/ReplayService\";\nimport { GameplayClock } from \"../common/game/GameplayClock\";\nimport { BeatmapManager } from \"../manager/BeatmapManager\";\nimport { GameSimulator } from \"../common/game/GameSimulator\";\nimport { injectable } from \"inversify\";\nimport { PixiRendererManager } from \"../renderers/PixiRendererManager\";\nimport { AnalysisSceneManager } from \"../manager/AnalysisSceneManager\";\nimport { GameLoop } from \"../common/game/GameLoop\";\nimport { ScenarioManager } from \"../manager/ScenarioManager\";\nimport { ReplayFileWatcher } from \"../common/local/ReplayFileWatcher\";\nimport { OsuFolderService } from \"../common/local/OsuFolderService\";\nimport { ClipRecorder } from \"../manager/ClipRecorder\";\nimport { ModSettingsService } from \"./mod-settings\";\nimport { ScreenshotTaker } from \"./screenshot\";\n\n@injectable()\nexport class AnalysisApp {\n  constructor(\n    public readonly gameClock: GameplayClock,\n    public readonly gameSimulator: GameSimulator,\n    public readonly scenarioManager: ScenarioManager,\n    public readonly modSettingsService: ModSettingsService,\n    public readonly replayWatcher: ReplayFileWatcher,\n    public readonly screenshotTaker: ScreenshotTaker,\n    public readonly clipRecorder: ClipRecorder,\n    public readonly osuFolderService: OsuFolderService,\n    private readonly replayService: ReplayService,\n    private readonly gameLoop: GameLoop,\n    private readonly beatmapManager: BeatmapManager,\n    private readonly sceneManager: AnalysisSceneManager,\n    private readonly pixiRenderer: PixiRendererManager,\n  ) {}\n\n  stats() {\n    return this.gameLoop.stats();\n  }\n\n  initialize() {\n    console.log(\"AnalysisApp: Initialize\");\n    this.replayWatcher.startWatching();\n    this.scenarioManager.initialize();\n  }\n\n  close() {}\n\n  onEnter(canvas: HTMLCanvasElement) {\n    this.pixiRenderer.initializeRenderer(canvas);\n    // this.gameLoop.startTicker();\n  }\n\n  onHide() {\n    this.gameClock.pause();\n    this.pixiRenderer.destroy();\n    // this.gameLoop.stopTicker();\n  }\n\n  /**\n   * Loads the replay and the corresponding beatmap and makes the application ready to visualize the replay.\n   *\n   * Note: This procedure can be optimized in the future, but for now it's ok.\n   *\n   * @param replayId the id of the replay to load\n   */\n  async loadReplay(replayId: string) {\n    return this.scenarioManager.loadReplay(replayId);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/analysis-cursor.ts",
    "content": "import { PersistentService } from \"../core/service\";\nimport { injectable } from \"inversify\";\nimport { JSONSchemaType } from \"ajv\";\nimport { CursorSettings } from \"../common/cursor\";\n\nexport interface AnalysisCursorSettings extends CursorSettings {\n  colorKey1: number;\n  colorKey2: number;\n  colorNoKeys: number;\n  colorBothKeys: number;\n}\n\nexport const DEFAULT_ANALYSIS_CURSOR_SETTINGS: AnalysisCursorSettings = Object.freeze({\n  scale: 0.8,\n  enabled: true,\n  scaleWithCS: true,\n  colorNoKeys: 0x5d6463, // gray\n  colorKey1: 0xffa500, // orange)\n  colorKey2: 0x00ff00, // green)\n  colorBothKeys: 0x3cbdc1, // cyan)\n});\n\nexport const AnalysisCursorSettingsSchema: JSONSchemaType<AnalysisCursorSettings> = {\n  type: \"object\",\n  properties: {\n    scale: { type: \"number\", default: DEFAULT_ANALYSIS_CURSOR_SETTINGS.scale },\n    enabled: { type: \"boolean\", default: DEFAULT_ANALYSIS_CURSOR_SETTINGS.enabled },\n    scaleWithCS: { type: \"boolean\", default: DEFAULT_ANALYSIS_CURSOR_SETTINGS.scaleWithCS },\n    colorNoKeys: { type: \"number\", default: DEFAULT_ANALYSIS_CURSOR_SETTINGS.colorNoKeys },\n    colorKey1: { type: \"number\", default: DEFAULT_ANALYSIS_CURSOR_SETTINGS.colorKey1 },\n    colorKey2: { type: \"number\", default: DEFAULT_ANALYSIS_CURSOR_SETTINGS.colorKey2 },\n    colorBothKeys: { type: \"number\", default: DEFAULT_ANALYSIS_CURSOR_SETTINGS.colorBothKeys },\n  },\n  required: [],\n};\n\n@injectable()\nexport class AnalysisCursorSettingsStore extends PersistentService<AnalysisCursorSettings> {\n  key = \"analysis-cursor\";\n  schema: JSONSchemaType<AnalysisCursorSettings> = AnalysisCursorSettingsSchema;\n  getDefaultValue(): AnalysisCursorSettings {\n    return DEFAULT_ANALYSIS_CURSOR_SETTINGS;\n  }\n\n  setEnabled(enabled: boolean) {\n    this.changeSettings((s) => (s.enabled = enabled));\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/createRewindAnalysisApp.ts",
    "content": "import { Container } from \"inversify\";\nimport { AnalysisApp } from \"./AnalysisApp\";\nimport { PixiRendererManager } from \"../renderers/PixiRendererManager\";\nimport { GameplayClock } from \"../common/game/GameplayClock\";\nimport { EventEmitter2 } from \"eventemitter2\";\nimport { BeatmapManager } from \"../manager/BeatmapManager\";\nimport { ReplayManager } from \"../manager/ReplayManager\";\nimport { GameSimulator } from \"../common/game/GameSimulator\";\nimport { AnalysisSceneManager } from \"../manager/AnalysisSceneManager\";\nimport { SceneManager } from \"../common/scenes/SceneManager\";\nimport { GameLoop } from \"../common/game/GameLoop\";\nimport { AnalysisScene } from \"./scenes/AnalysisScene\";\nimport { BeatmapBackgroundFactory } from \"../renderers/components/background/BeatmapBackground\";\nimport { TextureManager } from \"../textures/TextureManager\";\nimport { AnalysisStage } from \"../renderers/components/stage/AnalysisStage\";\nimport { ForegroundHUDPreparer } from \"../renderers/components/hud/ForegroundHUDPreparer\";\nimport { PlayfieldFactory } from \"../renderers/components/playfield/PlayfieldFactory\";\nimport { PlayfieldBorderFactory } from \"../renderers/components/playfield/PlayfieldBorderFactory\";\nimport { HitObjectsContainerFactory } from \"../renderers/components/playfield/HitObjectsContainerFactory\";\nimport { HitCircleFactory } from \"../renderers/components/playfield/HitCircleFactory\";\nimport { SliderFactory } from \"../renderers/components/playfield/SliderFactory\";\nimport { SpinnerFactory } from \"../renderers/components/playfield/SpinnerFactory\";\nimport { SliderTextureManager } from \"../renderers/components/sliders/SliderTextureManager\";\nimport { CursorPreparer } from \"../renderers/components/playfield/CursorPreparer\";\nimport { JudgementPreparer } from \"../renderers/components/playfield/JudgementPreparer\";\nimport { AudioEngine } from \"../common/audio/AudioEngine\";\nimport { ScenarioManager } from \"../manager/ScenarioManager\";\nimport { ReplayFileWatcher } from \"../common/local/ReplayFileWatcher\";\nimport { ClipRecorder } from \"../manager/ClipRecorder\";\nimport { IdleScene } from \"./scenes/IdleScene\";\nimport { KeyPressWithNoteSheetPreparer } from \"../renderers/components/keypresses/KeyPressOverlay\";\nimport { ModSettingsService } from \"./mod-settings\";\nimport { ScreenshotTaker } from \"./screenshot\";\nimport { STAGE_TYPES } from \"../types\";\n\n/**\n * This is a Rewind specific constructor of the \"Analysis\" tool (not to be used outside of Rewind).\n *\n * Reason is that many \"Rewind\" tools share the same services in order to provide smoother experiences.\n *\n * Example: If I use the \"Cutter\" tool then I want to use the same preferred skin that is used across Rewind.\n *\n * The analysis tool can be used as a standalone app though.\n */\nexport function createRewindAnalysisApp(commonContainer: Container) {\n  const container = new Container({ defaultScope: \"Singleton\" });\n  container.parent = commonContainer;\n  container.bind(STAGE_TYPES.EVENT_EMITTER).toConstantValue(new EventEmitter2());\n\n  container.bind(ReplayManager).toSelf();\n  container.bind(BeatmapManager).toSelf();\n  container.bind(GameplayClock).toSelf();\n  container.bind(ScenarioManager).toSelf();\n  container.bind(ModSettingsService).toSelf();\n  container.bind(GameSimulator).toSelf();\n  container.bind(PixiRendererManager).toSelf();\n\n  // Plugins ?\n  container.bind(ScreenshotTaker).toSelf();\n  container.bind(ClipRecorder).toSelf();\n  container.bind(ReplayFileWatcher).toSelf();\n\n  // Assets\n  container.bind(TextureManager).toSelf();\n  container.bind(AudioEngine).toSelf();\n\n  // Scenes\n  container.bind(AnalysisSceneManager).toSelf();\n  container.bind(SceneManager).toSelf();\n\n  // Skin is given by above\n  // container.bind(SkinManager).toSelf();\n\n  // AnalysisScenes\n  container.bind(AnalysisScene).toSelf();\n  container.bind(IdleScene).toSelf();\n\n  // Sliders\n  container.bind(SliderTextureManager).toSelf();\n\n  container.bind(AnalysisStage).toSelf();\n  {\n    container.bind(BeatmapBackgroundFactory).toSelf();\n    container.bind(ForegroundHUDPreparer).toSelf();\n    container.bind(KeyPressWithNoteSheetPreparer).toSelf();\n    container.bind(PlayfieldFactory).toSelf();\n    {\n      container.bind(PlayfieldBorderFactory).toSelf();\n      container.bind(HitObjectsContainerFactory).toSelf();\n      container.bind(HitCircleFactory).toSelf();\n      container.bind(SliderFactory).toSelf();\n      container.bind(SpinnerFactory).toSelf();\n\n      container.bind(CursorPreparer).toSelf();\n      container.bind(JudgementPreparer).toSelf();\n    }\n  }\n\n  container.bind(GameLoop).toSelf();\n\n  container.bind(AnalysisApp).toSelf();\n  return container.get(AnalysisApp);\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/mod-settings.ts",
    "content": "import { injectable } from \"inversify\";\nimport { BehaviorSubject } from \"rxjs\";\n\ninterface AnalysisModSettings {\n  hidden: boolean;\n  flashlight: boolean;\n}\n\n@injectable()\nexport class ModSettingsService {\n  modSettings$: BehaviorSubject<AnalysisModSettings>;\n\n  constructor() {\n    this.modSettings$ = new BehaviorSubject<AnalysisModSettings>({\n      flashlight: false,\n      hidden: false,\n    });\n  }\n\n  get modSettings() {\n    return this.modSettings$.getValue();\n  }\n\n  setHidden(hidden: boolean) {\n    this.modSettings$.next({ ...this.modSettings, hidden });\n  }\n\n  setFlashlight(flashlight: boolean) {\n    this.modSettings$.next({ ...this.modSettings, flashlight });\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/scenes/AnalysisScene.ts",
    "content": "import { UserScene } from \"../../common/scenes/IScene\";\nimport { injectable } from \"inversify\";\nimport { AnalysisStage } from \"../../renderers/components/stage/AnalysisStage\";\nimport { GameplayClock } from \"../../common/game/GameplayClock\";\nimport { GameSimulator } from \"../../common/game/GameSimulator\";\n\n// Just the normal analysis scene that updates according to a virtual game clock.\n@injectable()\nexport class AnalysisScene implements UserScene {\n  constructor(\n    private readonly gameClock: GameplayClock,\n    private readonly gameSimulator: GameSimulator,\n    private readonly analysisStage: AnalysisStage,\n  ) {}\n\n  update() {\n    this.gameClock.tick();\n    this.gameSimulator.simulate(this.gameClock.timeElapsedInMs);\n    this.analysisStage.updateAnalysisStage();\n  }\n\n  get stage() {\n    return this.analysisStage.stage;\n  }\n\n  destroy(): void {\n    this.analysisStage.destroy();\n  }\n\n  init(data: unknown): void {\n    // Do nothing\n  }\n\n  preload(): Promise<void> {\n    return Promise.resolve(undefined);\n  }\n\n  create(): void {\n    // Do nothing\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/scenes/IdleScene.ts",
    "content": "// Usually if there is no replay loaded\n// Just show a text\n// In the future -> show a cool looking logo\n\nimport { UserScene } from \"../../common/scenes/IScene\";\nimport { Container } from \"pixi.js\";\nimport { injectable } from \"inversify\";\n\n@injectable()\nexport class IdleScene implements UserScene {\n  stage = new Container();\n\n  destroy(): void {\n    this.stage.destroy();\n  }\n\n  init(data: string): void {}\n\n  async preload(): Promise<void> {\n    // Do nothing\n  }\n\n  update(): void {\n    // Do nothing\n  }\n\n  create(): void {\n    this.stage = new Container();\n    // const text = new Text(\"Load a beatmap/replay to get started!\", {\n    //   fontSize: 16,\n    //   fill: 0xeeeeee,\n    //   fontFamily: \"Arial\",\n    //   align: \"left\",\n    // });\n    // // Maybe center it somewhere\n    // this.stage.addChild(text);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/scenes/LoadingScene.ts",
    "content": "// Very simple loading scene that shows the progress of the loading\nimport { UserScene } from \"../../common/scenes/IScene\";\nimport { Container, Text } from \"pixi.js\";\n\nexport class LoadingScene implements UserScene {\n  stage: Container = new Container();\n\n  destroy(): void {}\n\n  init(): void {\n    //\n  }\n\n  async preload(): Promise<void> {\n    //\n  }\n\n  update(): void {\n    //\n  }\n\n  create(): void {\n    this.stage = new Container();\n\n    const text = new Text(\"LoadingScene...\", {\n      fontSize: 16,\n      fill: 0xeeeeee,\n      fontFamily: \"Arial\",\n      align: \"left\",\n    });\n    // Maybe center it somewhere\n    this.stage.addChild(text);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/scenes/ResultsScreenScene.ts",
    "content": "// Should be shown after the replay or can be manually triggered\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/analysis/screenshot.ts",
    "content": "import { injectable } from \"inversify\";\nimport { AnalysisScene } from \"./scenes/AnalysisScene\";\nimport { PixiRendererManager } from \"../renderers/PixiRendererManager\";\n\n@injectable()\nexport class ScreenshotTaker {\n  constructor(private readonly analysisScene: AnalysisScene, private readonly pixiRenderer: PixiRendererManager) {\n  }\n\n  takeScreenshot() {\n    const renderer = this.pixiRenderer.getRenderer();\n    if (!renderer) return;\n\n    const canvas: HTMLCanvasElement = renderer.plugins.extract.canvas(this.analysisScene.stage);\n    canvas.toBlob(\n      (blob) => {\n        const a = document.createElement(\"a\");\n        a.download = `Rewind Screenshot ${new Date().toISOString()}.jpg`;\n        a.href = URL.createObjectURL(blob!);\n        a.click();\n        a.remove();\n      },\n      \"image/jpeg\",\n      0.9,\n    );\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/CommonManagers.ts",
    "content": "import { Container, injectable } from \"inversify\";\nimport { ReplayService } from \"./local/ReplayService\";\nimport { SkinLoader } from \"./local/SkinLoader\";\nimport { AudioService } from \"./audio/AudioService\";\nimport { createRewindAnalysisApp } from \"../analysis/createRewindAnalysisApp\";\nimport { AudioSettingsStore } from \"./audio/settings\";\nimport { BeatmapBackgroundSettingsStore } from \"./beatmap-background\";\nimport { PlayfieldBorderSettingsStore } from \"./playfield-border\";\nimport { AnalysisCursorSettingsStore } from \"../analysis/analysis-cursor\";\nimport { ReplayCursorSettingsStore } from \"./replay-cursor\";\nimport { SkinHolder, SkinManager, SkinSettingsStore } from \"./skin\";\nimport { HitErrorBarSettingsStore } from \"./hit-error-bar\";\nimport { PlaybarSettingsStore } from \"./playbar\";\nimport { OsuFolderService } from \"./local/OsuFolderService\";\nimport { OsuDBDao } from \"./local/OsuDBDao\";\nimport { BlueprintLocatorService } from \"./local/BlueprintLocatorService\";\nimport { BeatmapRenderService } from \"./beatmap-render\";\nimport { STAGE_TYPES } from \"../types\";\nimport { AppInfoService } from \"./app-info\";\n\n/**\n * Creates the services that support all the osu! tools such as the Analyzer.\n * Common settings are set here so that they can be shared with other tools.\n * Example: Preferred skin can be set at only one place and is shared among all tools.\n */\n@injectable()\nexport class CommonManagers {\n  constructor(\n    public readonly skinManager: SkinManager,\n    public readonly skinSettingsStore: SkinSettingsStore,\n    public readonly audioSettingsService: AudioSettingsStore,\n    public readonly beatmapBackgroundSettingsStore: BeatmapBackgroundSettingsStore,\n    public readonly beatmapRenderSettingsStore: BeatmapRenderService,\n    public readonly hitErrorBarSettingsStore: HitErrorBarSettingsStore,\n    public readonly analysisCursorSettingsStore: AnalysisCursorSettingsStore,\n    public readonly replayCursorSettingsStore: ReplayCursorSettingsStore,\n    public readonly playbarSettingsStore: PlaybarSettingsStore,\n    public readonly appInfoService: AppInfoService,\n  ) {}\n\n  async initialize() {\n    await this.skinManager.loadPreferredSkin();\n  }\n}\n\ninterface Settings {\n  rewindSkinsFolder: string;\n  appVersion: string;\n  appPlatform: string;\n}\n\nexport function createRewindTheater({ rewindSkinsFolder, appPlatform, appVersion }: Settings) {\n  // Regarding `skipBaseClassChecks`: https://github.com/inversify/InversifyJS/issues/522#issuecomment-682246076\n  const container = new Container({ defaultScope: \"Singleton\" });\n  container.bind(STAGE_TYPES.AUDIO_CONTEXT).toConstantValue(new AudioContext());\n  container.bind(STAGE_TYPES.REWIND_SKINS_FOLDER).toConstantValue(rewindSkinsFolder);\n  container.bind(STAGE_TYPES.APP_PLATFORM).toConstantValue(appPlatform);\n  container.bind(STAGE_TYPES.APP_VERSION).toConstantValue(appVersion);\n  container.bind(OsuFolderService).toSelf();\n  container.bind(OsuDBDao).toSelf();\n  container.bind(BlueprintLocatorService).toSelf();\n  container.bind(ReplayService).toSelf();\n  container.bind(SkinLoader).toSelf();\n  container.bind(SkinHolder).toSelf();\n  container.bind(AudioService).toSelf();\n  container.bind(SkinManager).toSelf();\n  container.bind(AppInfoService).toSelf();\n\n  // General settings stores\n  container.bind(AudioSettingsStore).toSelf();\n  container.bind(AnalysisCursorSettingsStore).toSelf();\n  container.bind(BeatmapBackgroundSettingsStore).toSelf();\n  container.bind(BeatmapRenderService).toSelf();\n  container.bind(HitErrorBarSettingsStore).toSelf();\n  container.bind(PlayfieldBorderSettingsStore).toSelf();\n  container.bind(ReplayCursorSettingsStore).toSelf();\n  container.bind(SkinSettingsStore).toSelf();\n  container.bind(PlaybarSettingsStore).toSelf();\n\n  // Theater facade\n  container.bind(CommonManagers).toSelf();\n\n  return {\n    common: container.get(CommonManagers),\n    analyzer: createRewindAnalysisApp(container),\n  };\n}\n\nexport type RewindTheater = ReturnType<typeof createRewindTheater>;\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/app-info.ts",
    "content": "import { inject, injectable } from \"inversify\";\nimport { STAGE_TYPES } from \"../types\";\n\n@injectable()\nexport class AppInfoService {\n  constructor(\n    @inject(STAGE_TYPES.APP_PLATFORM) public readonly platform: string,\n    @inject(STAGE_TYPES.APP_VERSION) public readonly version: string,\n  ) {\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/audio/AudioEngine.ts",
    "content": "import { inject, injectable, postConstruct } from \"inversify\";\nimport { AudioSettings, AudioSettingsStore } from \"./settings\";\nimport { STAGE_TYPES } from \"../../types\";\nimport { GameplayClock } from \"../game/GameplayClock\";\n\n// HTML5 Audio supports time stretching without pitch changing (otherwise sounds like night core)\n// Chromium's implementation of <audio> is the best.\n// HTML5 Audio currentTime sucks though in terms of accuracy\n\n// \"AudioEngine\"\n@injectable()\nexport class AudioEngine {\n  // Just like in osu!, we can change the volume of music and samples separately\n  musicGain: GainNode;\n  samplesGain: GainNode;\n  masterGain: GainNode;\n\n  musicUrl?: string;\n  musicBuffer?: AudioBuffer;\n\n  song?: MediaElementAudioSourceNode;\n\n  // In seconds\n  sampleWindow = 0.1;\n  schedulePointer = 0;\n\n  constructor(\n    private readonly audioSettingService: AudioSettingsStore,\n    @inject(STAGE_TYPES.AUDIO_CONTEXT) private readonly audioContext: AudioContext,\n    private readonly gameClock: GameplayClock,\n  ) {\n    this.masterGain = this.audioContext.createGain();\n    this.musicGain = this.audioContext.createGain();\n    this.musicGain.connect(this.masterGain);\n    this.samplesGain = this.audioContext.createGain();\n    this.samplesGain.connect(this.masterGain);\n    this.masterGain.connect(this.audioContext.destination);\n\n    // this.handleAudioSettingsChanged(this.audioSettingService.getSettings());\n  }\n\n  @postConstruct()\n  postConstruct() {\n    console.log(\"Initializing AudioEngine\");\n    this.setupListeners();\n  }\n\n  // async loadSong(songUrl: string) {\n  //   // Disconnect other one?\n  //   const audio = new HTMLAudioElement();\n  //   audio.crossOrigin = \"anonymous\";\n  //   audio.src = songUrl;\n  //   this.song = this.audioContext.createMediaElementSource(audio);\n  //   this.song.connect(this.musicGain);\n  // }\n  //\n  setSong(audio: HTMLAudioElement) {\n    this.song = this.audioContext.createMediaElementSource(audio);\n    this.song.connect(this.musicGain);\n  }\n\n  setupListeners() {\n    this.gameClock.seeked$.subscribe(this.seekTo.bind(this));\n    this.gameClock.isPlaying$.subscribe((isPlaying: boolean) => {\n      if (isPlaying) this.start();\n      else this.pause();\n    });\n    this.gameClock.speed$.subscribe(this.changePlaybackRate.bind(this));\n\n    this.audioSettingService.settings$.subscribe((value) => this.handleAudioSettingsChanged(value));\n  }\n\n  start() {\n    // TODO: ???\n    this.audioContext.resume();\n    this.song?.mediaElement.play();\n  }\n\n  pause() {\n    if (this.song) {\n      this.song.mediaElement.pause();\n      this.schedulePointer = 0;\n    }\n  }\n\n  changePlaybackRate(newPlaybackRate: number) {\n    if (this.song) {\n      this.song.mediaElement.playbackRate = newPlaybackRate;\n    }\n  }\n\n  // Don't know if this is accurate *enough*\n  currentTime() {\n    return (this.song?.mediaElement.currentTime ?? 0) * 1000;\n  }\n\n  seekTo(toInMs: number) {\n    if (this.song) {\n      this.song.mediaElement.currentTime = toInMs / 1000;\n    }\n  }\n\n  get isPlaying() {\n    return !this.song?.mediaElement.paused;\n  }\n\n  togglePlaying(): boolean {\n    if (this.isPlaying) this.pause();\n    else this.start();\n    return this.isPlaying;\n  }\n\n  get durationInMs() {\n    return (this.song?.mediaElement.duration ?? 1) * 1000;\n  }\n\n  destroy() {\n    if (this.song) {\n      this.song.disconnect();\n      this.song = undefined;\n    }\n    // this.pause();\n    // this.audioContext.close().then(() => {\n    //   console.log(\"Audio context closed.\");\n    // });\n  }\n\n  private handleAudioSettingsChanged(settings: AudioSettings) {\n    const { muted, volume } = settings;\n    this.masterGain.gain.value = muted ? 0 : volume.master;\n    this.musicGain.gain.value = volume.music;\n    this.samplesGain.gain.value = volume.effects;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/audio/AudioService.ts",
    "content": "import { injectable } from \"inversify\";\n\n/**\n * Only one AudioService?\n * Only one AudioContext.\n */\n@injectable()\nexport class AudioService {\n  private audioContext: AudioContext;\n\n  audios: Record<string, HTMLAudioElement> = {};\n\n  constructor() {\n    this.audioContext = new AudioContext();\n  }\n\n  async loadAudio(filePath: string) {\n    const songUrl = filePath;\n    const audio = new Audio();\n    audio.crossOrigin = \"anonymous\";\n    audio.src = songUrl;\n    return audio;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/audio/settings.ts",
    "content": "import { injectable } from \"inversify\";\nimport { PersistentService } from \"../../core/service\";\nimport { JSONSchemaType } from \"ajv\";\n\nexport interface AudioSettings {\n  muted: boolean;\n  volume: {\n    master: number;\n    music: number;\n    effects: number;\n  };\n}\n\nexport const DEFAULT_AUDIO_SETTINGS: AudioSettings = Object.freeze({\n  muted: false,\n  volume: {\n    // Make it intentionally low, but not muted.\n    master: 0.1,\n    music: 1.0,\n    effects: 0.25,\n  },\n});\nexport const AudioSettingsSchema: JSONSchemaType<AudioSettings> = {\n  type: \"object\",\n  properties: {\n    muted: { type: \"boolean\", default: DEFAULT_AUDIO_SETTINGS.muted },\n    volume: {\n      type: \"object\",\n      properties: {\n        master: { type: \"number\", default: DEFAULT_AUDIO_SETTINGS.volume.master },\n        music: { type: \"number\", default: DEFAULT_AUDIO_SETTINGS.volume.music },\n        effects: { type: \"number\", default: DEFAULT_AUDIO_SETTINGS.volume.effects },\n      },\n      required: [],\n    },\n  },\n  required: [],\n};\n\n@injectable()\nexport class AudioSettingsStore extends PersistentService<AudioSettings> {\n  key = \"audio-settings\";\n  schema = AudioSettingsSchema;\n\n  getDefaultValue(): AudioSettings {\n    return DEFAULT_AUDIO_SETTINGS;\n  }\n\n  toggleMuted() {\n    this.changeSettings((d) => (d.muted = !d.muted));\n  }\n\n  setMasterVolume(volume: number) {\n    this.changeSettings((d) => (d.volume.master = volume));\n  }\n\n  setMusicVolume(volume: number) {\n    this.changeSettings((d) => (d.volume.music = volume));\n  }\n\n  setEffectsVolume(volume: number) {\n    this.changeSettings((d) => (d.volume.effects = volume));\n  }\n\n  setMuted(muted: boolean) {\n    this.changeSettings((d) => (d.muted = muted));\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/beatmap-background.ts",
    "content": "import { injectable } from \"inversify\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { Texture } from \"pixi.js\";\nimport { PersistentService } from \"../core/service\";\nimport { JSONSchemaType } from \"ajv\";\n\nexport interface BeatmapBackgroundSettings {\n  // Whether it should be rendered or not. Temporarily disabling with this flag gives a better UX than setting the dim\n  // to 100% since the user can retain the old value.\n  enabled: boolean;\n  // A number between 0 and 1. The resulting blur strength equals `blur * MAX_BLUR_STRENGTH`.\n  blur: number;\n  // dim = 1 - alpha\n  dim: number;\n}\n\nexport const DEFAULT_BEATMAP_BACKGROUND_SETTINGS: BeatmapBackgroundSettings = Object.freeze({\n  dim: 0.8,\n  blur: 0.4,\n  enabled: true,\n});\nexport const BeatmapBackgroundSettingsSchema: JSONSchemaType<BeatmapBackgroundSettings> = {\n  type: \"object\",\n  properties: {\n    dim: { type: \"number\", default: DEFAULT_BEATMAP_BACKGROUND_SETTINGS.dim },\n    blur: { type: \"number\", default: DEFAULT_BEATMAP_BACKGROUND_SETTINGS.blur },\n    enabled: { type: \"boolean\", default: DEFAULT_BEATMAP_BACKGROUND_SETTINGS.enabled },\n  },\n  required: [],\n};\n\n@injectable()\nexport class BeatmapBackgroundSettingsStore extends PersistentService<BeatmapBackgroundSettings> {\n  key = \"beatmap-background\";\n  schema = BeatmapBackgroundSettingsSchema;\n\n  // TODO: Move?\n  texture$ = new BehaviorSubject<Texture>(Texture.EMPTY);\n\n  getDefaultValue(): BeatmapBackgroundSettings {\n    return DEFAULT_BEATMAP_BACKGROUND_SETTINGS;\n  }\n\n  get texture() {\n    return this.texture$.getValue();\n  }\n\n  setBlur(blur: number) {\n    this.settings$.next({ ...this.settings, blur });\n  }\n\n  setDim(dim: number) {\n    this.settings$.next({ ...this.settings, dim });\n  }\n\n  setEnabled(enabled: boolean) {\n    this.settings$.next({ ...this.settings, enabled });\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/beatmap-render.ts",
    "content": "import { JSONSchemaType } from \"ajv\";\nimport { injectable } from \"inversify\";\nimport { PersistentService } from \"../core/service\";\n\nexport interface BeatmapRenderSettings {\n  sliderDevMode: boolean;\n  // By default, osu! will ALWAYS draw slider ends, but most skins have an invisible slider end texture.\n  drawSliderEnds: boolean;\n}\n\nexport const DEFAULT_BEATMAP_RENDER_SETTINGS: BeatmapRenderSettings = Object.freeze({\n  sliderDevMode: false,\n  drawSliderEnds: false,\n});\n\nexport const BeatmapRenderSettingsSchema: JSONSchemaType<BeatmapRenderSettings> = {\n  type: \"object\",\n  properties: {\n    sliderDevMode: { type: \"boolean\", default: DEFAULT_BEATMAP_RENDER_SETTINGS.sliderDevMode },\n    drawSliderEnds: { type: \"boolean\", default: DEFAULT_BEATMAP_RENDER_SETTINGS.drawSliderEnds },\n  },\n  required: [],\n};\n\n@injectable()\nexport class BeatmapRenderService extends PersistentService<BeatmapRenderSettings> {\n  key = \"beatmap-render\";\n  schema = BeatmapRenderSettingsSchema;\n\n  getDefaultValue(): BeatmapRenderSettings {\n    return DEFAULT_BEATMAP_RENDER_SETTINGS;\n  }\n  setSliderDevMode(sliderDevMode: boolean) {\n    this.changeSettings((draft) => (draft.sliderDevMode = sliderDevMode));\n  }\n\n  setDrawSliderEnds(drawSliderEnds: boolean) {\n    this.changeSettings((draft) => (draft.drawSliderEnds = drawSliderEnds));\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/cursor.ts",
    "content": "export interface CursorSettings {\n  // A number between 0.1 and 2.0\n  scale: number;\n  // If it should be shown or not\n  enabled: boolean;\n  // If it should scale with the circle size of the beatmap\n  scaleWithCS: boolean;\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/game/GameLoop.ts",
    "content": "import * as PIXI from \"pixi.js\";\nimport { GameplayClock } from \"./GameplayClock\";\nimport { PixiRendererManager } from \"../../renderers/PixiRendererManager\";\nimport { injectable, postConstruct } from \"inversify\";\nimport { SceneManager } from \"../scenes/SceneManager\";\nimport MrDoobStats from \"stats.js\";\n\nfunction defaultMonitor() {\n  const s = new MrDoobStats();\n  s.dom.style.position = \"absolute\";\n  s.dom.style.left = \"0px\";\n  s.dom.style.top = \"0px\";\n  s.dom.style.zIndex = \"9000\";\n  // if (props.initialPanel !== undefined) s.showPanel(props.initialPanel);\n  return s;\n}\n\n// Game loop does not have to stop if the game clock is paused.\n// For example, we could still toggle hidden on/off and need to see the changes on the canvas.\n// However, it should be paused, if the view is destroyed or if the window was blurred ...\n// Default behavior is to stop the game loop in case of window.blur because usually the player does not want to\n// watch the replay in Rewind while playing osu!.\n// End game behavior would be to automatically stop the game loop if the user is playing osu!, which we\n// could detect through some memory reader.\n@injectable()\nexport class GameLoop {\n  private ticker: PIXI.Ticker;\n  private readonly performanceMonitor: MrDoobStats;\n\n  constructor(\n    private gameClock: GameplayClock, // Maybe also inject?\n    private sceneManager: SceneManager,\n    private pixiRendererService: PixiRendererManager,\n  ) {\n    this.ticker = new PIXI.Ticker();\n    this.performanceMonitor = defaultMonitor();\n  }\n\n  stats() {\n    return this.performanceMonitor.dom;\n  }\n\n  @postConstruct()\n  initializeTicker() {\n    this.ticker.add(this.tickHandler.bind(this));\n  }\n\n  startTicker() {\n    this.ticker.start();\n  }\n\n  stopTicker() {\n    this.ticker.stop();\n  }\n\n  private update(deltaTimeMs: number) {\n    this.sceneManager.update(deltaTimeMs);\n  }\n\n  private render() {\n    const renderer = this.pixiRendererService.getRenderer();\n    if (renderer) {\n      this.pixiRendererService.resizeCanvasToDisplaySize();\n      renderer.render(this.sceneManager.createStage());\n    }\n  }\n\n  // deltaTimeMs will be given by PixiTicker\n  tickHandler(deltaTimeMs: number) {\n    // Maybe we can measure the `.update()` and the `.render()` independently\n    // console.debug(`Updating with timeDelta=${deltaTimeMs}ms`);\n    this.performanceMonitor.begin();\n    this.update(deltaTimeMs);\n    this.render();\n    this.performanceMonitor.end();\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/game/GameSimulator.ts",
    "content": "import {\n  Beatmap,\n  BucketedGameStateTimeMachine,\n  defaultGameplayInfo,\n  GameplayInfo,\n  GameplayInfoEvaluator,\n  GameState,\n  HitObjectJudgement,\n  isHitObjectJudgement,\n  ReplayAnalysisEvent,\n  retrieveEvents,\n} from \"@osujs/core\";\nimport { injectable } from \"inversify\";\nimport type { OsuReplay } from \"../../../model/OsuReplay\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { parser, std_diff } from \"ojsama\";\nimport { Queue } from \"typescript-collections\";\nimport { max } from \"simple-statistics\";\n\n@injectable()\nexport class GameSimulator {\n  private gameplayTimeMachine?: BucketedGameStateTimeMachine;\n  private gameplayEvaluator?: GameplayInfoEvaluator;\n  private currentState?: GameState;\n  private lastState?: GameState;\n  private currentInfo: GameplayInfo = defaultGameplayInfo;\n  public replayEvents$: BehaviorSubject<ReplayAnalysisEvent[]>;\n  public difficulties$: BehaviorSubject<number[]>;\n  public judgements: HitObjectJudgement[] = [];\n  public hits: [number, number, boolean][] = [];\n\n  constructor() {\n    this.replayEvents$ = new BehaviorSubject<ReplayAnalysisEvent[]>([]);\n    this.difficulties$ = new BehaviorSubject<number[]>([]);\n  }\n\n  calculateDifficulties(rawBeatmap: string, durationInMs: number, mods: number) {\n    console.log(`Calculating difficulty for beatmap with duration=${durationInMs}ms and mods=${mods}`);\n    const p = new parser();\n    p.feed(rawBeatmap);\n    const map = p.map;\n    const d = new std_diff().calc({ map, mods });\n\n    const TIME_STEP = 500;\n    const q = new Queue<[number, number]>();\n    let i = 0;\n    let sum = 0;\n    const res: number[] = [];\n\n    // O(n + m)\n    for (let t = 0; t < durationInMs; t += TIME_STEP) {\n      while (i < map.objects.length) {\n        const o = map.objects[i];\n        if (t + TIME_STEP < o.time) {\n          break;\n        }\n        const strainTotal = d.objects[i].strains[0] + d.objects[i].strains[1];\n        q.enqueue([o.time, strainTotal]);\n        sum += strainTotal;\n        i++;\n      }\n      while (!q.isEmpty()) {\n        const [time, totalStrain] = q.peek() as [number, number];\n        if (time > t - TIME_STEP) {\n          break;\n        }\n        sum -= totalStrain;\n        q.dequeue();\n      }\n      res.push(q.isEmpty() ? 0 : sum / q.size());\n    }\n    if (res.length > 0) {\n      // normalize\n      const m = max(res);\n      if (m > 0) {\n        const normalizedRes = res.map((r) => r / m);\n        this.difficulties$.next(normalizedRes);\n      }\n    }\n  }\n\n  calculateHitErrorArray() {}\n\n  simulateReplay(beatmap: Beatmap, replay: OsuReplay) {\n    this.gameplayTimeMachine = new BucketedGameStateTimeMachine(replay.frames, beatmap, {\n      hitWindowStyle: \"OSU_STABLE\",\n      noteLockStyle: \"STABLE\",\n    });\n    this.gameplayEvaluator = new GameplayInfoEvaluator(beatmap, {});\n    // TODO: Move this to async ...\n    this.lastState = this.gameplayTimeMachine.gameStateAt(1e9);\n    this.currentInfo = defaultGameplayInfo;\n    // this.currentState = finalState...\n    this.replayEvents$.next(retrieveEvents(this.lastState, beatmap.hitObjects));\n    this.judgements = this.replayEvents$.getValue().filter(isHitObjectJudgement);\n\n    this.hits = [];\n    if (!this.lastState) return;\n\n    // In order\n    for (const id of this.lastState.judgedObjects) {\n      const h = beatmap.getHitObject(id);\n      if (h.type === \"HIT_CIRCLE\") {\n        const s = this.lastState.hitCircleVerdict[id];\n        const hitCircle = beatmap.getHitCircle(id);\n        const offset = s.judgementTime - hitCircle.hitTime;\n        const hit = s.type !== \"MISS\";\n        this.hits.push([s.judgementTime, offset, hit]);\n      }\n    }\n    // not sure if this is needed\n    this.hits.sort((a, b) => a[0] - b[0]);\n  }\n\n  // Simulates the game to be at the given time\n  // If a whole game simulation has happened, then this should be really fast\n  simulate(gameTimeInMs: number) {\n    if (this.gameplayTimeMachine && this.gameplayEvaluator) {\n      this.currentState = this.gameplayTimeMachine.gameStateAt(gameTimeInMs);\n      this.currentInfo = this.gameplayEvaluator.evaluateReplayState(this.currentState!);\n    }\n  }\n\n  getCurrentState() {\n    return this.currentState;\n  }\n\n  getCurrentInfo() {\n    return this.currentInfo;\n  }\n\n  // Very likely to be a request from the UI since it wants to render the playbar events\n  async calculateEvents() {\n    // In case it takes unbearably long -> we might need a web worker\n  }\n\n  clear() {\n    this.replayEvents$.next([]);\n    this.difficulties$.next([]);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/game/GameplayClock.ts",
    "content": "import { injectable } from \"inversify\";\nimport { BehaviorSubject, Subject } from \"rxjs\";\n\nconst getNowInMs = () => performance.now();\n\n@injectable()\nexport class GameplayClock {\n  public isPlaying$: BehaviorSubject<boolean>;\n  public durationInMs$: BehaviorSubject<number>;\n  public speed$: BehaviorSubject<number>;\n\n  public seeked$: Subject<number>;\n\n  // Things that get updated very often should not be Subjects due to performance issues\n  public timeElapsedInMs = 0;\n  private lastUpdateTimeInMs = 0;\n\n  // private eventEmitter: EventEmitter;\n\n  constructor() {\n    this.isPlaying$ = new BehaviorSubject<boolean>(false);\n    this.durationInMs$ = new BehaviorSubject<number>(0);\n    this.speed$ = new BehaviorSubject<number>(1);\n    this.seeked$ = new Subject<number>();\n  }\n\n  /**\n   * Tick should only be used once in each frame and for the rest of the frame one should refer to `timeElapsedInMs`.\n   * This is to ensure that every related game object is referring to the same time at the same time.\n   */\n  tick() {\n    this.updateTimeElapsed();\n    if (this.timeElapsedInMs > this.durationInMs) {\n      this.pause();\n      this.timeElapsedInMs = this.durationInMs;\n    }\n    return this.timeElapsedInMs;\n  }\n\n  get speed() {\n    return this.speed$.getValue();\n  }\n\n  set speed(value: number) {\n    this.speed$.next(value);\n  }\n\n  get durationInMs() {\n    return this.durationInMs$.getValue();\n  }\n\n  set durationInMs(value: number) {\n    this.durationInMs$.next(Number.isNaN(value) ? 0 : value);\n  }\n\n  get isPlaying() {\n    return this.isPlaying$.getValue();\n  }\n\n  set isPlaying(value: boolean) {\n    this.isPlaying$.next(value);\n  }\n\n  updateTimeElapsed() {\n    if (!this.isPlaying) return;\n    const nowInMs = getNowInMs();\n    const deltaInMs = this.speed * (nowInMs - this.lastUpdateTimeInMs);\n    this.timeElapsedInMs += deltaInMs;\n    this.lastUpdateTimeInMs = nowInMs;\n  }\n\n  toggle() {\n    if (this.isPlaying) this.pause();\n    else this.start();\n  }\n\n  start() {\n    if (this.isPlaying) return;\n\n    // TODO: Maybe\n    // Resets it back to 0 in case the user wants to start the clock again when it already ended.\n    if (this.timeElapsedInMs >= this.durationInMs) {\n      // Will also emit an event\n      this.seekTo(0);\n    }\n    this.isPlaying = true;\n    this.lastUpdateTimeInMs = getNowInMs();\n  }\n\n  pause() {\n    if (!this.isPlaying) return;\n    this.updateTimeElapsed();\n    this.isPlaying = false;\n  }\n\n  setSpeed(speed: number) {\n    this.updateTimeElapsed();\n    this.speed = speed;\n  }\n\n  setDuration(durationInMs: number) {\n    console.debug(`GameClock duration has been set to ${durationInMs}ms`);\n    this.durationInMs = durationInMs;\n  }\n\n  seekTo(timeInMs: number) {\n    timeInMs = Math.min(this.durationInMs, Math.max(0, timeInMs));\n    this.timeElapsedInMs = timeInMs;\n    this.lastUpdateTimeInMs = getNowInMs();\n    this.seeked$.next(timeInMs);\n  }\n\n  clear() {\n    this.pause();\n    this.speed$.next(1.0);\n    // This is just ahot fix\n    this.durationInMs$.next(1);\n    this.timeElapsedInMs = 0;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/hit-error-bar.ts",
    "content": "import { injectable } from \"inversify\";\nimport { PersistentService } from \"../core/service\";\nimport { JSONSchemaType } from \"ajv\";\n\nexport interface HitErrorBarSettings {\n  enabled: boolean;\n  scale: number;\n}\n\nexport const DEFAULT_HIT_ERROR_BAR_SETTINGS: HitErrorBarSettings = Object.freeze({\n  enabled: true,\n  scale: 2.0,\n});\n\nexport const HitErrorBarSettingsSchema: JSONSchemaType<HitErrorBarSettings> = {\n  type: \"object\",\n  properties: {\n    enabled: { type: \"boolean\", default: DEFAULT_HIT_ERROR_BAR_SETTINGS.enabled },\n    scale: { type: \"number\", default: DEFAULT_HIT_ERROR_BAR_SETTINGS.scale },\n  },\n  required: [],\n};\n\n@injectable()\nexport class HitErrorBarSettingsStore extends PersistentService<HitErrorBarSettings> {\n  key = \"hit-error\";\n  schema = HitErrorBarSettingsSchema;\n\n  getDefaultValue() {\n    return DEFAULT_HIT_ERROR_BAR_SETTINGS;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/key-press-overlay.ts",
    "content": "export interface KeyPressOverlaySettings {\n  // The key presses that are relative to [-timeWindow+currentTime, currentTime+timeWindow] will be shown\n  timeWindow: number;\n\n  // Number between 0 and 1 determining the opacity of the right hand side\n  futureOpacity: number;\n}\n\n// export class KeyPressOverlaySettingsStore extends AbstractSettingsStore<KeyPressOverlaySettings> {}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/local/BlueprintLocatorService.ts",
    "content": "import { promises as fsPromises } from \"fs\";\nimport { Blueprint, BlueprintSection, parseBlueprint } from \"@osujs/core\";\nimport { join } from \"path\";\nimport { createHash } from \"crypto\";\nimport { filterFilenamesInDirectory } from \"@rewind/osu-local/utils\";\nimport { injectable } from \"inversify\";\nimport { OsuFolderService } from \"./OsuFolderService\";\nimport { BlueprintInfo } from \"../../../model/BlueprintInfo\";\nimport { OsuDBDao } from \"./OsuDBDao\";\n\nconst { stat, readFile } = fsPromises;\n\nconst METADATA_SECTIONS_TO_READ: BlueprintSection[] = [\"General\", \"Difficulty\", \"Events\", \"Metadata\"];\n\nfunction mapToLocalBlueprint(\n  blueprint: Blueprint,\n  osuFileName: string,\n  folderName: string,\n  md5Hash: string,\n): BlueprintInfo {\n  const { metadata } = blueprint.blueprintInfo;\n  return {\n    creator: \"\", // TODO: ?\n    title: metadata.title,\n    osuFileName,\n    folderName,\n    bgFileName: metadata.backgroundFile,\n    md5Hash,\n    audioFileName: metadata.audioFile,\n    artist: metadata.artist,\n    lastPlayed: new Date(),\n  };\n}\n\n// Basically just the identifier of the blueprint\ntype BlueprintMD5 = string;\n\n/**\n * Service responsible for locating the blueprints.\n *\n * It mainly uses the `osu!.db` to get the majority of the blueprints. However, once the user imports new beatmaps,\n * the `osu!.db` doesn't get immediately updated (no flushing), thus we have to find out which ones were added by\n * manually looking for new files.\n */\n@injectable()\nexport class BlueprintLocatorService {\n  private blueprints: Record<BlueprintMD5, BlueprintInfo> = {};\n\n  constructor(private readonly osuDbDao: OsuDBDao, private readonly osuFolderService: OsuFolderService) {\n  }\n\n  private get songsFolder() {\n    return this.osuFolderService.songsFolder$.getValue();\n  }\n\n  private async completeRead() {\n    const freshBlueprints = await this.osuDbDao.getAllBlueprints();\n    const lastModifiedTime = await this.osuDbDao.getOsuDbLastModifiedTime();\n\n    console.log(`Reading the osu!/Songs folder: ${this.songsFolder}`);\n    const candidates = await getNewFolderCandidates(this.songsFolder, new Date(lastModifiedTime));\n    if (candidates.length > 0) {\n      console.log(`New blueprint candidates (${candidates.length}) : ${candidates.join(\",\")}`);\n    }\n\n    for (const songFolder of candidates) {\n      const osuFiles = await listOsuFiles(join(this.songsFolder, songFolder));\n      for (const osuFile of osuFiles) {\n        const fileName = join(this.songsFolder, songFolder, osuFile);\n        console.log(`Reading file ${fileName}`);\n        const data = await readFile(fileName);\n        const hash = createHash(\"md5\");\n        hash.update(data);\n        const md5Hash = hash.digest(\"hex\");\n        const blueprint = await parseBlueprint(data.toString(\"utf-8\"), { sectionsToRead: METADATA_SECTIONS_TO_READ });\n        freshBlueprints.push(mapToLocalBlueprint(blueprint, osuFile, songFolder, md5Hash));\n      }\n    }\n\n    this.blueprints = {};\n    freshBlueprints.forEach(this.addNewBlueprint.bind(this));\n    return this.blueprints;\n  }\n\n  // Usually for watchers\n  private addNewBlueprint(blueprint: BlueprintInfo) {\n    this.blueprints[blueprint.md5Hash] = blueprint;\n  }\n\n  async blueprintHasBeenAdded() {\n    // It's better to rely on the \"osu!.db\" than the ones we received from watching.\n    // But ofc, we if we read from osu!.db again there might still be some folders that have not been properly flushed.\n    if (await this.osuDbDao.hasChanged()) {\n      return true;\n    }\n    const s = await stat(this.songsFolder);\n    const t = await this.osuDbDao.getOsuDbLastModifiedTime();\n    return s.mtime.getTime() > t;\n  }\n\n  async getAllBlueprints(): Promise<Record<string, BlueprintInfo>> {\n    const needToCheckAgain = await this.blueprintHasBeenAdded();\n    if (needToCheckAgain) {\n      await this.completeRead();\n    }\n    return this.blueprints;\n  }\n\n  async getBlueprintByMD5(md5: string): Promise<BlueprintInfo | undefined> {\n    const maps = await this.getAllBlueprints();\n    return maps[md5];\n  }\n\n  public async blueprintBg(md5: string): Promise<string | undefined> {\n    const blueprintMetaData = await this.getBlueprintByMD5(md5);\n    if (!blueprintMetaData) return undefined;\n    const { osuFileName, folderName } = blueprintMetaData;\n    const data = await readFile(join(this.songsFolder, folderName, osuFileName), { encoding: \"utf-8\" });\n    const parsedBlueprint = parseBlueprint(data, { sectionsToRead: [\"Events\"] });\n    // There is also an offset but let's ignore for now\n    return parsedBlueprint.blueprintInfo.metadata.backgroundFile;\n  }\n}\n\n// This might be a very expensive operation and should be done only once at startup. The new ones should be watched\n// ~250ms at my Songs folder\nexport async function getNewFolderCandidates(songsFolder: string, importedLaterThan: Date): Promise<string[]> {\n  return filterFilenamesInDirectory(songsFolder, async (fileName) => {\n    const s = await stat(join(songsFolder, fileName));\n    return s.mtime.getTime() > importedLaterThan.getTime() && s.isDirectory();\n  });\n}\n\nexport async function listOsuFiles(songFolder: string): Promise<string[]> {\n  return filterFilenamesInDirectory(songFolder, async (fileName) => fileName.endsWith(\".osu\"));\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/local/OsuDBDao.ts",
    "content": "import { join } from \"path\";\nimport { Beatmap as DBBeatmap, OsuDBReader } from \"@rewind/osu-local/db-reader\";\nimport { fileLastModifiedTime, ticksToDate } from \"@rewind/osu-local/utils\";\nimport { readFile } from \"fs/promises\";\nimport { BlueprintInfo } from \"../../../model/BlueprintInfo\";\nimport { OsuFolderService } from \"./OsuFolderService\";\nimport { injectable } from \"inversify\";\n\nconst mapToBlueprint = (b: DBBeatmap): BlueprintInfo => {\n  return {\n    md5Hash: b.md5Hash,\n    audioFileName: b.audioFileName,\n    folderName: b.folderName,\n    lastPlayed: ticksToDate(b.lastPlayed)[0],\n    osuFileName: b.fileName,\n    artist: b.artist, // unicode?\n    title: b.title,\n    creator: b.creator,\n  };\n};\n\n@injectable()\nexport class OsuDBDao {\n  private lastMtime = -1;\n  private blueprints: BlueprintInfo[] = [];\n\n  constructor(private readonly osuFolderService: OsuFolderService) {\n    // We just assume that we need to read it again\n    this.osuFolderService.settings$.subscribe(() => {\n      this.lastMtime = -1;\n      this.blueprints = [];\n    });\n  }\n\n  private get osuDbPath() {\n    return join(this.osuFolderService.getOsuFolder(), \"osu!.db\");\n  }\n\n  private async createReader() {\n    const buffer = await readFile(this.osuDbPath);\n    return new OsuDBReader(buffer);\n  }\n\n  async getOsuDbLastModifiedTime() {\n    return await fileLastModifiedTime(this.osuDbPath);\n  }\n\n  async hasChanged(): Promise<boolean> {\n    return this.cachedTime !== (await this.getOsuDbLastModifiedTime());\n  }\n\n  get cachedTime() {\n    return this.lastMtime;\n  }\n\n  async getAllBlueprints(): Promise<BlueprintInfo[]> {\n    const lastModified = await this.getOsuDbLastModifiedTime();\n    if (lastModified === this.lastMtime) {\n      return this.blueprints;\n    }\n    console.log(`Reading the osu!.db with lastModifiedTime=${lastModified}`);\n\n    this.lastMtime = lastModified;\n    const reader = await this.createReader();\n    const osuDB = await reader.readOsuDB();\n    return (this.blueprints = osuDB.beatmaps.map(mapToBlueprint));\n  }\n\n  async getBlueprintByMD5(md5: string): Promise<BlueprintInfo | undefined> {\n    const maps = await this.getAllBlueprints();\n    return maps.find((m) => m.md5Hash === md5);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/local/OsuFolderService.ts",
    "content": "import { injectable } from \"inversify\";\nimport { access } from \"fs/promises\";\nimport { join } from \"path\";\nimport { constants } from \"fs\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { determineSongsFolder } from \"@rewind/osu-local/utils\";\nimport username from \"username\";\nimport { ipcRenderer } from \"electron\";\nimport { PersistentService } from \"../../core/service\";\nimport { JSONSchemaType } from \"ajv\";\n\nconst filesToCheck = [\"osu!.db\", \"scores.db\", \"Skins\"];\n\n/**\n * Checks certain files to see if Rewind can be booted without any problems with the given `osuFolderPath`.\n * @param osuFolderPath the folder path to check the files in\n */\nexport async function osuFolderSanityCheck(osuFolderPath: string) {\n  try {\n    await Promise.all(filesToCheck.map((f) => access(join(osuFolderPath, f), constants.R_OK)));\n  } catch (err) {\n    console.log(err);\n    return false;\n  }\n  return true;\n}\n\ninterface OsuSettings {\n  osuStablePath: string;\n}\n\nexport const DEFAULT_OSU_SETTINGS: OsuSettings = Object.freeze({\n  osuStablePath: \"\",\n});\nexport const OsuSettingsSchema: JSONSchemaType<OsuSettings> = {\n  type: \"object\",\n  properties: {\n    osuStablePath: { type: \"string\", default: DEFAULT_OSU_SETTINGS.osuStablePath },\n  },\n  required: [],\n};\n\n@injectable()\nexport class OsuFolderService extends PersistentService<OsuSettings> {\n  public replaysFolder$ = new BehaviorSubject<string>(\"\");\n  public songsFolder$ = new BehaviorSubject<string>(\"\");\n\n  key = \"osu-settings\";\n  schema = OsuSettingsSchema;\n\n  constructor() {\n    super();\n    this.settings$.subscribe(this.onFolderChange.bind(this));\n  }\n\n  getDefaultValue(): OsuSettings {\n    return DEFAULT_OSU_SETTINGS;\n  }\n\n  async onFolderChange(osuSettings: OsuSettings) {\n    const { osuStablePath } = osuSettings;\n    ipcRenderer.send(\"osuFolderChanged\", osuStablePath);\n    this.replaysFolder$.next(join(osuStablePath, \"Replays\"));\n    const userId = await username();\n    this.songsFolder$.next((await determineSongsFolder(osuStablePath, userId as string)) as string);\n  }\n\n  getOsuFolder(): string {\n    return this.settings.osuStablePath;\n  }\n\n  setOsuFolder(path: string) {\n    console.log(`osu! folder was set to '${path}'`);\n    this.changeSettings((draft) => (draft.osuStablePath = path));\n  }\n\n  async isValidOsuFolder(directoryPath: string) {\n    return osuFolderSanityCheck(directoryPath);\n  }\n\n  async hasValidOsuFolderSet(): Promise<boolean> {\n    return this.isValidOsuFolder(this.getOsuFolder());\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/local/ReplayFileWatcher.ts",
    "content": "import { injectable } from \"inversify\";\nimport { Subject } from \"rxjs\";\nimport * as chokidar from \"chokidar\";\nimport { OsuFolderService } from \"./OsuFolderService\";\n\n@injectable()\nexport class ReplayFileWatcher {\n  public readonly newReplays$: Subject<string>;\n  private watcher?: chokidar.FSWatcher;\n\n  constructor(private readonly osuFolderService: OsuFolderService) {\n    this.newReplays$ = new Subject<string>();\n  }\n\n  public startWatching() {\n    this.osuFolderService.replaysFolder$.subscribe(this.onNewReplayFolder.bind(this));\n  }\n\n  // Unsubscribes from the old replay folder and starts listening on the new folder that was given\n  // TODO: In the future we might want to watch on a list of folders and not just the osu! folder\n  private onNewReplayFolder(folder: string) {\n    // For now, we just use .close()\n    // We could make it cleaner by using .unwatch() and adding new files to watch\n    if (this.watcher) {\n      void this.watcher.close();\n    }\n\n    const globPattern = folder;\n    console.log(`Watching for replays (.osr) in folder: ${folder} with pattern: ${globPattern}`);\n    this.watcher = chokidar.watch(globPattern, {\n      // ignoreInitial must be true otherwise addDir will be triggered for every folder initially.\n      ignoreInitial: true,\n      persistent: true,\n      depth: 0, // if somehow osu! is trolling, this will prevent it\n    });\n    this.watcher.on(\"ready\", () => {\n      console.log(\"\");\n    });\n    this.watcher.on(\"add\", (path) => {\n      if (!path.endsWith(\".osr\")) {\n        return;\n      }\n      console.log(`Detected new file at: ${path}`);\n      this.newReplays$.next(path);\n    });\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/local/ReplayService.ts",
    "content": "import { modsFromBitmask, parseReplayFramesFromRaw } from \"@osujs/core\";\nimport { injectable } from \"inversify\";\nimport { OsuReplay } from \"../../../model/OsuReplay\";\nimport { ipcRenderer } from \"electron\";\n\nexport type REPLAY_SOURCES = \"OSU_API\" | \"FILE\";\n\n@injectable()\nexport class ReplayService {\n  async retrieveReplay(replayId: string, source: REPLAY_SOURCES = \"FILE\"): Promise<OsuReplay> {\n    const filePath = replayId;\n    // const res = await readOsr(filePath);\n    // Currently using readOsr is bugged, so we need to use it from the main process\n    const res = await ipcRenderer.invoke(\"readOsr\", filePath);\n    return {\n      gameVersion: res.gameVersion,\n      frames: parseReplayFramesFromRaw(res.replay_data),\n      mods: modsFromBitmask(res.mods),\n      md5hash: res.replayMD5,\n      beatmapMd5: res.beatmapMD5,\n      player: res.playerName,\n    };\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/local/SkinLoader.ts",
    "content": "import { Loader } from \"@pixi/loaders\";\nimport { Texture } from \"pixi.js\";\nimport { DEFAULT_SKIN_TEXTURE_CONFIG, OsuSkinTextures } from \"@rewind/osu/skin\";\nimport { inject, injectable } from \"inversify\";\nimport { Skin, SkinTexturesByKey } from \"../../../model/Skin\";\nimport { SkinId } from \"../../../model/SkinId\";\nimport { OsuFolderService } from \"./OsuFolderService\";\nimport { GetTextureFileOption, OsuSkinTextureResolver, SkinFolderReader } from \"@rewind/osu-local/skin-reader\";\nimport { join } from \"path\";\nimport { STAGE_TYPES } from \"../../types\";\n\nexport type SkinTextureLocation = { key: OsuSkinTextures; paths: string[] };\n\nasync function startLoading(loader: Loader, skinName: string): Promise<boolean> {\n  return new Promise<boolean>((resolve, reject) => {\n    loader.onStart.once(() => {\n      console.log(`Skin loading started for ${skinName}`);\n    });\n\n    loader.onComplete.once(() => {\n      console.debug(`Skin loading completed for ${skinName}`);\n      resolve(true);\n    });\n\n    loader.onError.once((resource) => {\n      console.error(`Could not load resource ${resource.name}`);\n    });\n    loader.load();\n  });\n}\n\nconst OSU_DEFAULT_SKIN_ID: SkinId = { source: \"rewind\", name: \"OsuDefaultSkin\" };\n\n@injectable()\nexport class SkinLoader {\n  skins: { [key: string]: Skin };\n  skinElementCounter = 0;\n\n  // Maybe generalize it to skinSource or something\n  // TODO: Maybe just load into TextureManager\n  constructor(\n    private osuFolderService: OsuFolderService,\n    @inject(STAGE_TYPES.REWIND_SKINS_FOLDER) private rewindSkinsFolder: string,\n  ) {\n    this.skins = {};\n  }\n\n  // TODO: AppResourcesPath and get the included Rewind Skins\n\n  async loadSkinList() {\n    return SkinFolderReader.listSkinsInFolder(join(this.osuFolderService.getOsuFolder(), \"Skins\"), {\n      skinIniRequired: false,\n    });\n  }\n\n  sourcePath(source: string) {\n    switch (source) {\n      case \"rewind\":\n        return this.rewindSkinsFolder;\n      case \"osu\":\n        return join(this.osuFolderService.getOsuFolder(), \"Skins\");\n    }\n    return \"\";\n  }\n\n  resolveToPath({ source, name }: SkinId) {\n    return join(this.sourcePath(source), name);\n  }\n\n  /**\n   * In the future we also want to get the skin info with the beatmap as the parameters in order to retrieve\n   * beatmap related skin files as well.\n   */\n  async resolve(\n    osuSkinTexture: OsuSkinTextures,\n    options: GetTextureFileOption,\n    list: { prefix: string; resolver: OsuSkinTextureResolver }[],\n  ) {\n    for (const { prefix, resolver } of list) {\n      const filePaths = await resolver.resolve(osuSkinTexture, options);\n      if (filePaths.length === 0) {\n        continue;\n      }\n      return filePaths.map((path) => `${prefix}/${path}`);\n    }\n    console.debug(`No skin has the skin texture ${osuSkinTexture}`);\n    return [];\n  }\n\n  // force such like reloading\n  async loadSkin(skinId: SkinId, forceReload?: boolean): Promise<Skin> {\n    const id = `${skinId.source}/${skinId.name}`;\n    if (this.skins[id] && !forceReload) {\n      console.info(`Skin ${id} is already loaded, using the one in cache`);\n      return this.skins[id];\n    }\n    console.log(`Loading skin with name: ${skinId.name} with source ${skinId.source}`);\n    const loader = new Loader();\n\n    const osuDefaultSkinResolver = await SkinFolderReader.getSkinResolver(this.resolveToPath(OSU_DEFAULT_SKIN_ID));\n    const skinResolver = await SkinFolderReader.getSkinResolver(this.resolveToPath(skinId));\n\n    const { config } = skinResolver;\n\n    const skinTextureKeys = Object.keys(DEFAULT_SKIN_TEXTURE_CONFIG);\n    const files = await Promise.all(\n      skinTextureKeys.map(async (key) => ({\n        key,\n        paths: await this.resolve(key as OsuSkinTextures, { animatedIfExists: true, hdIfExists: true }, [\n          // In the future beatmap stuff can be listed here as well\n          {\n            prefix: join(\"file://\", this.resolveToPath(skinId)),\n            resolver: skinResolver,\n          },\n          {\n            prefix: join(\"file://\", this.resolveToPath(OSU_DEFAULT_SKIN_ID)),\n            resolver: osuDefaultSkinResolver,\n          },\n        ]),\n      })),\n    );\n\n    const textures: SkinTexturesByKey = {};\n    const skinName = config.general.name;\n\n    // Every file, even non-animatable files have the index in their name\n    // For example \"WhiteCat/hitcircle-0\" would be one key name\n\n    const queueFiles: { key: OsuSkinTextures; name: string }[] = [];\n    files.forEach((stl) => {\n      stl.paths.forEach((path, index) => {\n        // `Loader` will die if the same `name` gets used twice therefore the unique skinElementCounter\n        // Maybe even use a timestamp\n        const name = `${this.skinElementCounter++}/${skinName}/${stl.key}-${index}`;\n        loader.add(name, path);\n        queueFiles.push({ key: stl.key as OsuSkinTextures, name });\n      });\n    });\n\n    const loaded = await startLoading(loader, skinName);\n\n    if (!loaded) {\n      throw new Error(`${skinName} not loaded correctly`);\n    }\n\n    queueFiles.forEach((file) => {\n      if (!(file.key in textures)) textures[file.key] = [];\n      textures[file.key]?.push(loader.resources[file.name].texture as Texture);\n    });\n\n    return (this.skins[id] = new Skin(config, textures));\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/local-storage.ts",
    "content": "// Will store the settings after a couple seconds\nimport Ajv, { JSONSchemaType, ValidateFunction } from \"ajv\";\n\nconst ajv = new Ajv({ useDefaults: true });\n\nexport class LocalStorageLoader<T> {\n  validate: ValidateFunction<T>;\n\n  constructor(private readonly localStorageKey: string, schema: JSONSchemaType<T>, private readonly defaultValue: T) {\n    this.validate = ajv.compile<T>(schema);\n  }\n\n  loadFromLocalStorage(): T {\n    const { validate, localStorageKey, defaultValue } = this;\n    const str = window.localStorage.getItem(localStorageKey);\n    if (str === null) {\n      // This is very expected on first time start up\n      return defaultValue;\n    }\n    let json;\n    try {\n      json = JSON.parse(str);\n    } catch (e) {\n      console.warn(`Could not parse '${localStorageKey}' as a JSON properly: ${str}`);\n      return defaultValue;\n    }\n\n    if (!validate(json)) {\n      console.warn(`JSON '${localStorageKey}' was not validated properly: ${JSON.stringify(validate.errors)}`);\n      return defaultValue;\n    } else {\n      return json;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/playbar.ts",
    "content": "import { PersistentService } from \"../core/service\";\nimport { injectable } from \"inversify\";\nimport { JSONSchemaType } from \"ajv\";\n\nexport interface PlaybarSettings {\n  difficultyGraphEnabled: boolean;\n}\n\nexport const DEFAULT_PLAY_BAR_SETTINGS: PlaybarSettings = Object.freeze({\n  difficultyGraphEnabled: true,\n});\n\nexport const PlaybarSettingsSchema: JSONSchemaType<PlaybarSettings> = {\n  type: \"object\",\n  properties: {\n    difficultyGraphEnabled: { type: \"boolean\", default: DEFAULT_PLAY_BAR_SETTINGS.difficultyGraphEnabled },\n  },\n  required: [],\n};\n\n@injectable()\nexport class PlaybarSettingsStore extends PersistentService<PlaybarSettings> {\n  key = \"playbar\";\n  schema = PlaybarSettingsSchema;\n\n  getDefaultValue(): PlaybarSettings {\n    return DEFAULT_PLAY_BAR_SETTINGS;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/playfield-border.ts",
    "content": "import { injectable } from \"inversify\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { PlayfieldBorderSettings } from \"@rewind/osu-pixi/classic-components\";\n\nconst DEFAULT_SETTINGS: PlayfieldBorderSettings = {\n  enabled: true,\n  thickness: 2,\n};\n\n// TODO: Make this configurable, currently it's just hardcoded\n\n@injectable()\nexport class PlayfieldBorderSettingsStore {\n  settings$: BehaviorSubject<PlayfieldBorderSettings>;\n\n  constructor() {\n    this.settings$ = new BehaviorSubject<PlayfieldBorderSettings>(DEFAULT_SETTINGS);\n  }\n\n  get settings() {\n    return this.settings$.getValue();\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/replay-cursor.ts",
    "content": "import { PersistentService } from \"../core/service\";\nimport { JSONSchemaType } from \"ajv\";\nimport { CursorSettings } from \"./cursor\";\nimport { injectable } from \"inversify\";\n\nexport interface ReplayCursorSettings extends CursorSettings {\n  showTrail: boolean;\n  smoothCursorTrail: boolean;\n}\n\nexport const DEFAULT_REPLAY_CURSOR_SETTINGS: ReplayCursorSettings = Object.freeze({\n  showTrail: true,\n  scale: 0.8,\n  enabled: true,\n  scaleWithCS: true,\n  smoothCursorTrail: true,\n});\nexport const ReplayCursorSettingsSchema: JSONSchemaType<ReplayCursorSettings> = {\n  type: \"object\",\n  properties: {\n    showTrail: { type: \"boolean\", default: DEFAULT_REPLAY_CURSOR_SETTINGS.showTrail },\n    scale: { type: \"number\", default: DEFAULT_REPLAY_CURSOR_SETTINGS.scale },\n    enabled: { type: \"boolean\", default: DEFAULT_REPLAY_CURSOR_SETTINGS.enabled },\n    scaleWithCS: { type: \"boolean\", default: DEFAULT_REPLAY_CURSOR_SETTINGS.scaleWithCS },\n    smoothCursorTrail: { type: \"boolean\", default: DEFAULT_REPLAY_CURSOR_SETTINGS.smoothCursorTrail },\n  },\n  required: [],\n};\n\n@injectable()\nexport class ReplayCursorSettingsStore extends PersistentService<ReplayCursorSettings> {\n  key = \"replay-cursor\";\n  schema = ReplayCursorSettingsSchema;\n\n  getDefaultValue(): ReplayCursorSettings {\n    return DEFAULT_REPLAY_CURSOR_SETTINGS;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/scenes/IScene.ts",
    "content": "// Scene that needs to be implemented by the user\n// Ideas taken from Phaser3 https://rexrainbow.github.io/phaser3-rex-notes/docs/site/scene/\nimport { Container } from \"pixi.js\";\n\nexport interface UserScene {\n  // Initialize\n  init(data: unknown): void;\n\n  // Loading assets\n  preload(): Promise<void>;\n\n  // Creates stage objects\n  create(): void;\n\n  // This function is updated every tick.\n  update(deltaTimeInMs: number): void;\n\n  // Free resources, unsubscribe from events, etc.\n  destroy(): void;\n\n  stage: Container;\n}\n\n// Not to be implemented by the user\n\ntype ManagedSceneState = \"INITIALIZING\" | \"PLAYING\" | \"PAUSED\" | \"SLEEPING\";\n\nexport class ManagedScene implements UserScene {\n  state: ManagedSceneState = \"INITIALIZING\";\n\n  constructor(private readonly scene: UserScene, public readonly key: string) {}\n\n  get stage() {\n    return this.scene.stage;\n  }\n\n  pause() {\n    this.state = \"PAUSED\";\n  }\n\n  resume() {\n    this.state = \"PLAYING\";\n  }\n\n  sleep() {\n    this.state = \"SLEEPING\";\n  }\n\n  destroy(): void {\n    return this.scene.destroy();\n  }\n\n  init(data: unknown): void {\n    return this.scene.init(data);\n  }\n\n  async preload(): Promise<void> {\n    return this.scene.preload();\n  }\n\n  update(deltaTimeInMs: number): void {\n    if (this.state === \"PLAYING\") {\n      // console.log(`Scene ${this.key} is getting updated with dt=${deltaTimeInMs}ms`);\n      return this.scene.update(deltaTimeInMs);\n    }\n  }\n\n  create(): void {\n    return this.scene.create();\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/scenes/SceneManager.ts",
    "content": "// Manages the scene\nimport { Container } from \"pixi.js\";\nimport { ManagedScene, UserScene } from \"./IScene\";\nimport { injectable } from \"inversify\";\n\n@injectable()\nexport class SceneManager {\n  managedScene: ManagedScene[] = [];\n  container: Container;\n\n  constructor() {\n    this.container = new Container();\n  }\n\n  add(scene: UserScene, key: string) {\n    const managedScene = new ManagedScene(scene, key);\n    this.managedScene.push(managedScene);\n  }\n\n  async start(scene: ManagedScene, data: unknown) {\n    console.log(`Starting the scene with the key='${scene.key}'`);\n    scene.state = \"INITIALIZING\";\n    scene.init(data);\n    await scene.preload();\n    scene.create();\n    scene.state = \"PLAYING\";\n  }\n\n  stop(scene: ManagedScene) {\n    scene.destroy();\n    scene.state = \"SLEEPING\";\n  }\n\n  update(deltaTimeMs: number) {\n    this.managedScene.forEach((scene) => scene.update(deltaTimeMs));\n  }\n\n  sceneByKey(key: string) {\n    return this.managedScene.find((s) => s.key === key);\n  }\n\n  createStage() {\n    this.container.removeChildren();\n    this.managedScene.forEach((scene) => {\n      if (scene.state === \"PLAYING\" || scene.state === \"PAUSED\") {\n        this.container.addChild(scene.stage);\n      }\n    });\n    return this.container;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/common/skin.ts",
    "content": "import { PersistentService } from \"../core/service\";\nimport { injectable } from \"inversify\";\nimport { JSONSchemaType } from \"ajv\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { SkinLoader } from \"./local/SkinLoader\";\nimport { SkinId, skinIdToString, stringToSkinId } from \"../../model/SkinId\";\nimport { Skin } from \"../../model/Skin\";\n\nexport interface SkinSettings {\n  // fallbackSkinId is rewind:OsuDefaultSkin\n  preferredSkinId: string;\n}\n\nexport const DEFAULT_SKIN_SETTINGS: SkinSettings = Object.freeze({\n  preferredSkinId: \"rewind:RewindDefaultSkin\",\n});\nexport const SkinSettingsSchema: JSONSchemaType<SkinSettings> = {\n  type: \"object\",\n  properties: {\n    preferredSkinId: { type: \"string\", default: DEFAULT_SKIN_SETTINGS.preferredSkinId },\n  },\n  required: [\"preferredSkinId\"],\n};\n\n@injectable()\nexport class SkinSettingsStore extends PersistentService<SkinSettings> {\n  key = \"Skin\";\n  schema = SkinSettingsSchema;\n\n  getDefaultValue(): SkinSettings {\n    return DEFAULT_SKIN_SETTINGS;\n  }\n\n  setPreferredSkinId(preferredSkinId: string) {\n    this.changeSettings((s) => (s.preferredSkinId = preferredSkinId));\n  }\n}\n\n@injectable()\nexport class SkinHolder {\n  private skin: Skin;\n\n  constructor() {\n    this.skin = Skin.EMPTY;\n  }\n\n  getSkin(): Skin {\n    return this.skin;\n  }\n\n  setSkin(skin: Skin) {\n    this.skin = skin;\n  }\n}\n\n@injectable()\nexport class SkinManager {\n  skinList$: BehaviorSubject<string[]>;\n\n  constructor(\n    private readonly skinLoader: SkinLoader,\n    private readonly skinSettingsStore: SkinSettingsStore,\n    private readonly skinHolder: SkinHolder,\n  ) {\n    this.skinList$ = new BehaviorSubject<string[]>([]);\n  }\n\n  async loadSkin(skinId: SkinId) {\n    const skin = await this.skinLoader.loadSkin(skinId);\n    this.skinSettingsStore.setPreferredSkinId(skinIdToString(skinId));\n    this.skinHolder.setSkin(skin);\n  }\n\n  async loadPreferredSkin() {\n    const { preferredSkinId } = this.skinSettingsStore.settings;\n\n    try {\n      console.log(`Loading preferred skin from preferences: ${preferredSkinId}`);\n      const skinId = stringToSkinId(preferredSkinId);\n      await this.loadSkin(skinId);\n    } catch (e) {\n      console.error(`Could not load preferred skin '${preferredSkinId}' so falling back to default`);\n      this.skinSettingsStore.setPreferredSkinId(DEFAULT_SKIN_SETTINGS.preferredSkinId);\n      await this.loadSkin(stringToSkinId(DEFAULT_SKIN_SETTINGS.preferredSkinId));\n    }\n  }\n\n  async loadSkinList() {\n    this.skinList$.next(await this.skinLoader.loadSkinList());\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/core/service.ts",
    "content": "import { BehaviorSubject } from \"rxjs\";\nimport { Draft } from \"immer/dist/types/types-external\";\nimport produce from \"immer\";\nimport { injectable, postConstruct } from \"inversify\";\nimport { LocalStorageLoader } from \"../common/local-storage\";\nimport { JSONSchemaType } from \"ajv\";\nimport { debounceTime } from \"rxjs/operators\";\n\n@injectable()\nexport abstract class StatefulService<T> {\n  settings$: BehaviorSubject<T>;\n\n  constructor() {\n    this.settings$ = new BehaviorSubject<T>(this.getDefaultValue());\n  }\n\n  abstract getDefaultValue(): T;\n\n  getSettings() {\n    return this.settings;\n  }\n\n  changeSettings(fn: (draft: Draft<T>) => unknown) {\n    this.settings = produce(this.settings, (draft) => {\n      fn(draft);\n      // We explicitly return here so that `fn` can return anything\n      return;\n    });\n  }\n\n  getSubject() {\n    return this.settings$;\n  }\n\n  get settings() {\n    return this.settings$.getValue();\n  }\n\n  set settings(s: T) {\n    this.settings$.next(s);\n  }\n}\n\nconst DEBOUNCE_TIME_IN_MS = 500;\n\n@injectable()\nexport abstract class PersistentService<T> extends StatefulService<T> {\n  abstract key: string;\n  abstract schema: JSONSchemaType<T>;\n\n  @postConstruct()\n  init() {\n    const localStorageLoader = new LocalStorageLoader<T>(this.key, this.schema, this.getDefaultValue());\n    this.settings$.next(localStorageLoader.loadFromLocalStorage());\n    this.settings$.pipe(debounceTime(DEBOUNCE_TIME_IN_MS)).subscribe((s) => {\n      // Store the serialized version into the LocalStorage\n      window.localStorage.setItem(this.key, JSON.stringify(s));\n    });\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/manager/AnalysisSceneManager.ts",
    "content": "import { SceneManager } from \"../common/scenes/SceneManager\";\nimport { injectable } from \"inversify\";\nimport { AnalysisScene } from \"../analysis/scenes/AnalysisScene\";\nimport { IdleScene } from \"../analysis/scenes/IdleScene\";\nimport { ManagedScene } from \"../common/scenes/IScene\";\n\nexport enum AnalysisSceneKeys {\n  IDLE = \"idle\",\n  LOADING = \"loading\",\n  ANALYSIS = \"analysis\",\n}\n\n@injectable()\nexport class AnalysisSceneManager {\n  currentScene?: ManagedScene;\n\n  constructor(\n    private readonly sceneManager: SceneManager,\n    private readonly analysisScene: AnalysisScene,\n    private readonly idleScene: IdleScene,\n  ) {\n    sceneManager.add(analysisScene, AnalysisSceneKeys.ANALYSIS);\n    sceneManager.add(idleScene, AnalysisSceneKeys.IDLE);\n  }\n\n  async changeToScene(sceneKey: AnalysisSceneKeys, data?: any) {\n    if (this.currentScene) {\n      this.sceneManager.stop(this.currentScene);\n    }\n    const scene = this.sceneManager.sceneByKey(sceneKey);\n    if (scene) {\n      this.currentScene = scene;\n      return this.sceneManager.start(scene, data);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/manager/BeatmapManager.ts",
    "content": "// Holds the beatmap\nimport { Beatmap } from \"@osujs/core\";\nimport { injectable } from \"inversify\";\nimport { BehaviorSubject } from \"rxjs\";\n\n@injectable()\nexport class BeatmapManager {\n  beatmap$: BehaviorSubject<Beatmap> = new BehaviorSubject<Beatmap>(Beatmap.EMPTY_BEATMAP);\n\n  setBeatmap(beatmap: Beatmap) {\n    this.beatmap$.next(beatmap);\n  }\n\n  getBeatmap() {\n    return this.beatmap$.value;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/manager/ClipRecorder.ts",
    "content": "import { injectable } from \"inversify\";\nimport { PixiRendererManager } from \"../renderers/PixiRendererManager\";\nimport { BehaviorSubject } from \"rxjs\";\n\n// This is too slow\n@injectable()\nexport class ClipRecorder {\n  chunks: Blob[] = [];\n  recordingSince$: BehaviorSubject<number>;\n  recorder?: MediaRecorder;\n\n  constructor(private readonly renderManager: PixiRendererManager) {\n    this.recordingSince$ = new BehaviorSubject<number>(0);\n  }\n\n  startRecording() {\n    const renderer = this.renderManager.getRenderer();\n    if (!renderer) {\n      console.error(\"Can not start recording without a renderer\");\n      return;\n    }\n    this.recordingSince$.next(performance.now());\n    this.chunks = [];\n\n    const canvas = renderer.view;\n    const context = canvas.getContext(\"webgl2\");\n    // context.getImageData()\n    const frameRate = 30;\n\n    const stream = canvas.captureStream(frameRate);\n    this.recorder = new MediaRecorder(stream);\n    // Every time the recorder has new data, we will store it in our array\n    this.recorder.ondataavailable = (e) => this.chunks.push(e.data);\n    this.recorder.onstop = (e) => {\n      this.exportVideo();\n    };\n    this.recorder.start();\n  }\n\n  exportVideo() {\n    console.log(`Recording chunks.size = ${this.chunks.length}`);\n    this.recordingSince$.next(0);\n\n    const blob = new Blob(this.chunks, { type: \"video/webm\" });\n    const vid = document.createElement(\"video\");\n    vid.src = URL.createObjectURL(blob);\n    vid.controls = true;\n\n    const a = document.createElement(\"a\");\n    a.href = vid.src;\n    a.download = `Rewind Clip ${new Date().toISOString()}.webm`;\n    a.click();\n    a.remove();\n  }\n\n  stopRecording() {\n    this.recorder?.stop();\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/manager/ReplayManager.ts",
    "content": "// Holds the replays\nimport { injectable } from \"inversify\";\nimport { OsuReplay } from \"../../model/OsuReplay\";\nimport { BehaviorSubject } from \"rxjs\";\n\n@injectable()\nexport class ReplayManager {\n  // mainReplay: OsuReplay | null = null;\n  mainReplay$: BehaviorSubject<OsuReplay | null>;\n\n  constructor() {\n    this.mainReplay$ = new BehaviorSubject<OsuReplay | null>(null);\n  }\n\n  getMainReplay() {\n    return this.mainReplay$.value;\n  }\n\n  setMainReplay(replay: OsuReplay | null) {\n    this.mainReplay$.next(replay);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/manager/ScenarioManager.ts",
    "content": "import { injectable } from \"inversify\";\nimport { BehaviorSubject } from \"rxjs\";\nimport { Beatmap, buildBeatmap, modsToBitmask, parseBlueprint } from \"@osujs/core\";\nimport { GameSimulator } from \"../common/game/GameSimulator\";\nimport { AudioService } from \"../common/audio/AudioService\";\nimport { ReplayService } from \"../common/local/ReplayService\";\nimport { BeatmapManager } from \"./BeatmapManager\";\nimport { ReplayManager } from \"./ReplayManager\";\nimport { AnalysisSceneKeys, AnalysisSceneManager } from \"./AnalysisSceneManager\";\nimport { AudioEngine } from \"../common/audio/AudioEngine\";\nimport { GameplayClock } from \"../common/game/GameplayClock\";\nimport { GameLoop } from \"../common/game/GameLoop\";\nimport { PixiRendererManager } from \"../renderers/PixiRendererManager\";\nimport { join } from \"path\";\nimport { readFile } from \"fs/promises\";\nimport { BlueprintLocatorService } from \"../common/local/BlueprintLocatorService\";\nimport { OsuFolderService } from \"../common/local/OsuFolderService\";\nimport { BeatmapBackgroundSettingsStore } from \"../common/beatmap-background\";\nimport { TextureManager } from \"../textures/TextureManager\";\nimport { ReplayFileWatcher } from \"../common/local/ReplayFileWatcher\";\nimport { ModSettingsService } from \"../analysis/mod-settings\";\n\ninterface Scenario {\n  status: \"LOADING\" | \"ERROR\" | \"DONE\" | \"INIT\";\n}\n\nfunction localFile(path: string) {\n  return `file://${path}`;\n}\n\n@injectable()\nexport class ScenarioManager {\n  public scenario$: BehaviorSubject<Scenario>;\n\n  constructor(\n    private readonly gameClock: GameplayClock,\n    private readonly renderer: PixiRendererManager,\n    private readonly gameLoop: GameLoop,\n    private readonly gameSimulator: GameSimulator,\n    private readonly modSettingsService: ModSettingsService,\n    private readonly blueprintLocatorService: BlueprintLocatorService,\n    private readonly osuFolderService: OsuFolderService,\n    private readonly audioService: AudioService,\n    private readonly textureManager: TextureManager,\n    private readonly replayService: ReplayService,\n    private readonly beatmapBackgroundSettingsStore: BeatmapBackgroundSettingsStore,\n    private readonly beatmapManager: BeatmapManager,\n    private readonly replayManager: ReplayManager,\n    private readonly sceneManager: AnalysisSceneManager,\n    private readonly replayWatcher: ReplayFileWatcher,\n    private readonly audioEngine: AudioEngine,\n  ) {\n    this.scenario$ = new BehaviorSubject<Scenario>({ status: \"INIT\" });\n  }\n\n  public initialize() {\n    this.replayWatcher.newReplays$.subscribe((replayId) => {\n      void this.loadReplay(replayId);\n    });\n  }\n\n  // This is a temporary solution to\n  async clearReplay() {\n    this.gameClock.clear();\n    this.replayManager.setMainReplay(null);\n    this.audioEngine.destroy();\n    this.renderer.getRenderer()?.clear();\n    this.beatmapManager.setBeatmap(Beatmap.EMPTY_BEATMAP);\n    this.gameSimulator.clear();\n    this.gameLoop.stopTicker();\n    // await this.sceneManager.changeToScene(AnalysisSceneKeys.IDLE);\n    this.scenario$.next({ status: \"INIT\" });\n  }\n\n  async loadReplay(replayId: string) {\n    console.log(`ScenarioManager loading replay with id = ${replayId}`);\n    // TODO: Clean this up\n    this.audioEngine.destroy();\n\n    this.scenario$.next({ status: \"LOADING\" });\n\n    const replay = await this.replayService.retrieveReplay(replayId);\n    const blueprintInfo = await this.blueprintLocatorService.getBlueprintByMD5(replay.beatmapMd5);\n    if (!blueprintInfo) throw Error(`Could not find the blueprint with MD5=${replay.beatmapMd5}`);\n\n    const absoluteFolderPath = join(this.osuFolderService.songsFolder$.getValue(), blueprintInfo.folderName);\n\n    const rawBlueprint = await readFile(join(absoluteFolderPath, blueprintInfo.osuFileName), \"utf-8\");\n    const blueprint = parseBlueprint(rawBlueprint);\n\n    const { metadata } = blueprint.blueprintInfo;\n\n    // Load background\n    this.beatmapBackgroundSettingsStore.texture$.next(\n      await this.textureManager.loadTexture(localFile(join(absoluteFolderPath, metadata.backgroundFile))),\n    );\n\n    // Load audio\n    this.audioEngine.setSong(\n      await this.audioService.loadAudio(localFile(join(absoluteFolderPath, metadata.audioFile))),\n    );\n    this.audioEngine.song?.mediaElement.addEventListener(\"loadedmetadata\", () => {\n      const duration = (this.audioEngine.song?.mediaElement.duration ?? 0) * 1000;\n      this.gameClock.setDuration(duration);\n      this.gameSimulator.calculateDifficulties(rawBlueprint, duration, modsToBitmask(replay.mods));\n    });\n\n    // If the building is too slow or unbearable, we should push the building to a WebWorker, but right now it's ok\n    // even on long maps.\n    const beatmap = buildBeatmap(blueprint, { addStacking: true, mods: replay.mods });\n\n    console.log(`Beatmap built with ${beatmap.hitObjects.length} hitobjects`);\n    console.log(`Replay loaded with ${replay.frames.length} frames`);\n    const modHidden = replay.mods.includes(\"HIDDEN\");\n    const initialSpeed = beatmap.gameClockRate;\n\n    this.modSettingsService.setHidden(modHidden);\n    // Not supported yet\n    this.modSettingsService.setFlashlight(false);\n\n    this.gameClock.pause();\n    this.gameClock.setSpeed(initialSpeed);\n    this.gameClock.seekTo(0);\n    this.beatmapManager.setBeatmap(beatmap);\n    this.replayManager.setMainReplay(replay);\n\n    await this.gameSimulator.simulateReplay(beatmap, replay);\n    await this.sceneManager.changeToScene(AnalysisSceneKeys.ANALYSIS);\n\n    this.gameLoop.startTicker();\n    this.scenario$.next({ status: \"DONE\" });\n  }\n\n  // This is just the NM view of a beatmap\n  async loadBeatmap(blueprintId: string) {\n    // Set speed to 1.0\n    this.gameClock.setSpeed(1.0);\n    this.gameClock.seekTo(0);\n    this.modSettingsService.setHidden(false);\n    this.replayManager.setMainReplay(null);\n  }\n\n  async addSubReplay() {\n    // Only possible if `mainReplay` is loaded\n    // Adjust y-flip according to `mainReplay`\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/PixiRendererManager.ts",
    "content": "import * as PIXI from \"pixi.js\";\nimport { injectable } from \"inversify\";\n\n@injectable()\nexport class PixiRendererManager {\n  // RendererPreferenceSettingsService to get preferences\n  private renderer?: PIXI.Renderer;\n  private canvas?: HTMLCanvasElement;\n\n  initializeRenderer(canvas: HTMLCanvasElement) {\n    // Destroy old renderer\n    this.canvas = canvas;\n    this.renderer = new PIXI.Renderer({ view: canvas, antialias: true });\n    this.resizeCanvasToDisplaySize();\n  }\n\n  // https://webgl2fundamentals.org/webgl/lessons/webgl-anti-patterns.html\n  // https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth\n  resizeCanvasToDisplaySize() {\n    const canvas = this.canvas;\n    if (!canvas || !this.renderer) {\n      return false;\n    }\n    // If it's resolution does not match change it\n    if (canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientHeight) {\n      // canvas.width/height will be set by renderer.resize()\n      this.renderer.resize(canvas.clientWidth, canvas.clientHeight);\n      console.log(`Canvas dimensions have been set to ${canvas.width} x ${canvas.height}`);\n      return true;\n    }\n    return false;\n  }\n\n  destroy() {\n    this.renderer?.clear();\n    this.renderer?.destroy();\n    this.renderer = undefined;\n  }\n\n  getRenderer(): PIXI.Renderer | undefined {\n    return this.renderer;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/background/BeatmapBackground.ts",
    "content": "import { Sprite, Texture } from \"pixi.js\";\nimport { BlurFilter } from \"@pixi/filter-blur\";\n\nimport { injectable } from \"inversify\";\nimport { BeatmapBackgroundSettings, BeatmapBackgroundSettingsStore } from \"../../../common/beatmap-background\";\n\nconst MAX_BLUR_STRENGTH = 15;\n\ninterface Dimensions {\n  width: number;\n  height: number;\n}\n\nexport class BeatmapBackground {\n  public sprite: Sprite;\n\n  constructor(private readonly stageDimensions: Dimensions) {\n    this.sprite = new Sprite(); // No pooling needed\n  }\n\n  onSettingsChange(beatmapBackgroundSettings: BeatmapBackgroundSettings) {\n    const { enabled, dim, blur } = beatmapBackgroundSettings;\n    this.sprite.alpha = 1.0 - dim;\n    this.sprite.filters = [new BlurFilter(blur * MAX_BLUR_STRENGTH)];\n    this.sprite.renderable = enabled;\n  }\n\n  onTextureChange(texture: Texture) {\n    this.sprite.texture = texture;\n    // TODO: STAGE_WIDTH is kinda hardcoded\n    const scaling = this.stageDimensions.width / texture.width;\n    this.sprite.scale.set(scaling, scaling);\n  }\n}\n\n@injectable()\nexport class BeatmapBackgroundFactory {\n  constructor(private readonly settingsStore: BeatmapBackgroundSettingsStore) {}\n\n  createBeatmapBackground(stageDimensions: Dimensions) {\n    const beatmapBackground = new BeatmapBackground(stageDimensions);\n    this.settingsStore.settings$.subscribe((s) => beatmapBackground.onSettingsChange(s));\n    this.settingsStore.texture$.subscribe((t) => beatmapBackground.onTextureChange(t));\n    return beatmapBackground;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/hud/ForegroundHUDPreparer.ts",
    "content": "import { injectable } from \"inversify\";\nimport { GameSimulator } from \"../../../common/game/GameSimulator\";\nimport { Container, Text } from \"pixi.js\";\nimport {\n  calculateDigits,\n  OsuClassicAccuracy,\n  OsuClassicHitErrorBar,\n  OsuClassicNumber,\n} from \"@rewind/osu-pixi/classic-components\";\nimport { STAGE_HEIGHT, STAGE_WIDTH } from \"../../constants\";\nimport { formatGameTime, hitWindowsForOD } from \"@osujs/math\";\nimport { GameplayClock } from \"../../../common/game/GameplayClock\";\nimport { BeatmapManager } from \"../../../manager/BeatmapManager\";\nimport { mean, standardDeviation } from \"simple-statistics\";\nimport { HitErrorBarSettingsStore } from \"../../../common/hit-error-bar\";\nimport { SkinHolder } from \"../../../common/skin\";\n\nfunction calculateUnstableRate(x: number[]) {\n  return x.length === 0 ? 0 : standardDeviation(x) * 10;\n}\n\nfunction calculateMean(x: number[]) {\n  return x.length === 0 ? 0 : mean(x);\n}\n\n@injectable()\nexport class ForegroundHUDPreparer {\n  container: Container;\n  stats: Text;\n  hitErrorBar: OsuClassicHitErrorBar;\n\n  hitMinIndex = 0;\n  hitMaxIndex = 0;\n\n  // these are game clock rate adjusted\n  allHits: number[] = [];\n  recentHits: number[] = [];\n\n  constructor(\n    private readonly beatmapManager: BeatmapManager,\n    private readonly skinManager: SkinHolder,\n    private readonly gameSimulator: GameSimulator,\n    private readonly gameplayClock: GameplayClock,\n    private readonly hitErrorBarSettingsStore: HitErrorBarSettingsStore,\n  ) {\n    this.container = new Container();\n    this.stats = new Text(\"\", { fontSize: 16, fill: 0xeeeeee, fontFamily: \"Arial\", align: \"left\" });\n    this.hitErrorBar = new OsuClassicHitErrorBar();\n  }\n\n  updateHitErrorBar() {\n    const time = this.gameplayClock.timeElapsedInMs;\n\n    const beatmap = this.beatmapManager.getBeatmap();\n    const gameClockRate = beatmap.gameClockRate;\n    const maxHitErrorTime = 10000;\n    const hits: any[] = [];\n    this.allHits = [];\n    this.recentHits = [];\n    for (const [judgementTime, offset, hit] of this.gameSimulator.hits) {\n      const timeAgo = time - judgementTime;\n      if (timeAgo < 0) break;\n      if (timeAgo <= maxHitErrorTime) hits.push({ timeAgo: time - judgementTime, offset, miss: !hit });\n      if (hit) {\n        this.allHits.push(offset / gameClockRate);\n        if (timeAgo < 1000) this.recentHits.push(offset / gameClockRate);\n      }\n    }\n\n    // if (h.length === 0) {\n    //   return;\n    // }\n    // if (this.lastHits !== h) {\n    //   this.hitMinIndex = this.hitMaxIndex = 0;\n    //   this.lastHits = h;\n    // }\n\n    // TODO: This needs to be reset in case hits gets changed\n\n    // while (this.hitMaxIndex + 1 < h.length && h[this.hitMaxIndex + 1][0] <= time) this.hitMaxIndex++;\n    // while (this.hitMaxIndex >= 0 && h[this.hitMaxIndex][0] > time) this.hitMaxIndex--;\n    //\n    // while (this.hitMinIndex + 1 < h.length && h[this.hitMinIndex + 1][0] < time - maxTime) this.hitMinIndex++;\n    // while (this.hitMinIndex >= 0 && h[this.hitMinIndex][0] >= time - maxTime) this.hitMinIndex--;\n    //\n    // const hits: any[] = [];\n    // for (let i = this.hitMinIndex + 1; i < this.hitMaxIndex; i++) {\n    //   hits.push({ timeAgo: time - h[i][0], offset: h[i][1], miss: !h[i][2] });\n    // }\n    // console.log(hits.length);\n\n    const [hitWindow300, hitWindow100, hitWindow50] = hitWindowsForOD(beatmap.difficulty.overallDifficulty);\n    this.hitErrorBar.prepare({\n      hitWindow50,\n      hitWindow100,\n      hitWindow300,\n      hits,\n      // hits: [\n      //   { timeAgo: 100, offset: -2 },\n      //   { timeAgo: 2, offset: +10 },\n      // ],\n    });\n    this.hitErrorBar.container.position.set(STAGE_WIDTH / 2, STAGE_HEIGHT - 20);\n    this.hitErrorBar.container.scale.set(this.hitErrorBarSettingsStore.settings.scale);\n    this.container.addChild(this.hitErrorBar.container);\n  }\n\n  updateComboNumber() {\n    const skin = this.skinManager.getSkin();\n    const gameplayInfo = this.gameSimulator.getCurrentInfo();\n\n    if (gameplayInfo) {\n      const comboNumber = new OsuClassicNumber();\n      const textures = skin.getComboNumberTextures();\n      const overlap = skin.config.fonts.comboOverlap;\n      comboNumber.prepare({ digits: calculateDigits(gameplayInfo.currentCombo), textures, overlap });\n      comboNumber.position.set(0, STAGE_HEIGHT - 50);\n      this.container.addChild(comboNumber);\n    }\n  }\n\n  updateHUD() {\n    this.container.removeChildren();\n    this.updateComboNumber();\n    this.updateAccuracy();\n    this.updateHitErrorBar();\n    this.updateStats();\n  }\n\n  private updateAccuracy() {\n    const skin = this.skinManager.getSkin();\n    const gameplayInfo = this.gameSimulator.getCurrentInfo();\n    if (gameplayInfo) {\n      // const text\n      const accNumber = new OsuClassicAccuracy();\n      const digitTextures = skin.getScoreTextures();\n      const dotTexture = skin.getTexture(\"SCORE_DOT\");\n      const percentageTexture = skin.getTexture(\"SCORE_PERCENT\");\n      const overlap = skin.config.fonts.scoreOverlap;\n      accNumber.prepare({ accuracy: gameplayInfo.accuracy, digitTextures, dotTexture, percentageTexture, overlap });\n      accNumber.container.position.set(STAGE_WIDTH - 15, 25);\n      this.container.addChild(accNumber.container);\n    }\n  }\n\n  private updateStats() {\n    const gameplayInfo = this.gameSimulator.getCurrentInfo();\n    const gameplayState = this.gameSimulator.getCurrentState();\n    const time = this.gameplayClock.timeElapsedInMs;\n\n    if (gameplayInfo && gameplayState) {\n      const count = gameplayInfo.verdictCounts;\n      const maxCombo = gameplayInfo.maxComboSoFar;\n\n      const digits = 2;\n      const globalMean = calculateMean(this.allHits);\n      const globalDeviation = calculateUnstableRate(this.allHits);\n\n      const localMean = calculateMean(this.recentHits);\n      const localDeviation = calculateUnstableRate(this.recentHits);\n\n      this.stats.text = `Time: ${formatGameTime(time, true)}\n300: ${count[0]}\n100: ${count[1]}\n50: ${count[2]}\nMisses: ${count[3]}\n\nMaxCombo: ${maxCombo}\n\nGlobal\nUR: ${globalDeviation.toFixed(digits)}\nMean: ${globalMean.toFixed(digits)}ms\n\nLocal\nUR: ${localDeviation.toFixed(digits)}\nMean: ${localMean.toFixed(digits)}ms\n`;\n\n      this.stats.position.set(25, 50);\n      this.container.addChild(this.stats);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/keypresses/KeyPressOverlay.ts",
    "content": "import { TemporaryObjectPool } from \"../../../../utils/pooling/TemporaryObjectPool\";\nimport { Container, Graphics, Rectangle, Sprite, Texture } from \"pixi.js\";\nimport {\n  calculateReplayClicks,\n  isHitCircle,\n  MainHitObject,\n  OsuHitObject,\n  TimeInterval,\n  TimeIntervals,\n} from \"@osujs/core\";\nimport { injectable } from \"inversify\";\nimport { ReplayManager } from \"../../../manager/ReplayManager\";\nimport { GameplayClock } from \"../../../common/game/GameplayClock\";\nimport { BeatmapManager } from \"../../../manager/BeatmapManager\";\nimport { clamp } from \"@osujs/math\";\nimport { DEFAULT_ANALYSIS_CURSOR_SETTINGS } from \"../../../analysis/analysis-cursor\";\n\n/**\n *\n * Questions:\n * 1. Dynamic: depending on beatmap?\n * 2.\n * 3. HitCircles shown or not\n */\n\n// Can efficiently determine which time intervals are visible assuming that they are *static* and non intersecting such\n// as key presses.\n// More specifically, it finds all time intervals [start, end] that intersect with [time, time + timeWindow]\n\nfunction timeIntervalIntersection(a: TimeInterval, b: TimeInterval) {\n  return [Math.max(a[0], b[0]), Math.min(a[1], b[1])];\n}\n\nclass NonIntersectingTimeIntervalsTracker {\n  constructor(public intervals: TimeIntervals, public timeWindow: number) {}\n\n  /**\n   * Finds the intervals that intersect with time around the given timeWindow.\n   * @returns the indices\n   */\n  findVisibleIndices(time: number) {\n    // dummy implementation can be improved with sliding window\n    const w: TimeInterval = createTimeWindow(time, this.timeWindow);\n    const res: number[] = [];\n    for (let i = 0; i < this.intervals.length; i++) {\n      const t = timeIntervalIntersection(this.intervals[i], w);\n      if (t[0] <= t[1]) {\n        res.push(i);\n      }\n    }\n    return res;\n  }\n}\n\nconst WIDTH = 800;\nconst HEIGHT = 100;\nconst KEY_HEIGHT = 35;\n\nfunction positionInTimeline(currentTime: number, windowDuration: number, x: number) {\n  return (\n    ((x - currentTime + windowDuration) / (2 * windowDuration)) * // Percentage\n    WIDTH\n  );\n}\n\nexport class KeyPressOverlayRow {\n  spritePool: TemporaryObjectPool<Sprite>;\n  tracker: NonIntersectingTimeIntervalsTracker;\n  container: Container;\n  tint = 0xffffff;\n\n  constructor(timeIntervals: TimeIntervals, hitWindowTime: number) {\n    this.spritePool = new TemporaryObjectPool<Sprite>(\n      () => new Sprite(Texture.WHITE),\n      (g) => {\n        // unexpected arrow function\n      },\n      { initialSize: 10 },\n    );\n    this.tracker = new NonIntersectingTimeIntervalsTracker(timeIntervals, hitWindowTime);\n    this.container = new Container();\n  }\n\n  onTintChange(tint: number) {\n    this.tint = tint;\n  }\n\n  update(time: number) {\n    const intervalsIndices: number[] = this.tracker.findVisibleIndices(time);\n    const pastWindow: TimeInterval = [-this.tracker.timeWindow + time, time];\n    const futureWindow: TimeInterval = [time, time + this.tracker.timeWindow];\n\n    this.container.removeChildren();\n    const windowDuration = this.tracker.timeWindow;\n    const msToPx = (x: number) => positionInTimeline(time, windowDuration, x);\n\n    for (const i of intervalsIndices) {\n      const interval = this.tracker.intervals[i];\n\n      {\n        const pastIntersection = timeIntervalIntersection(pastWindow, interval);\n        if (pastIntersection[0] < pastIntersection[1]) {\n          const [sprite] = this.spritePool.allocate(\"pastTimeInterval\" + i);\n          // const sprite = new Sprite();\n          sprite.width = ((pastIntersection[1] - pastIntersection[0]) / (this.tracker.timeWindow * 2)) * WIDTH;\n          sprite.height = KEY_HEIGHT;\n          sprite.position.set(msToPx(pastIntersection[0]), 0);\n          sprite.alpha = 0.8;\n          sprite.tint = this.tint;\n          this.container.addChild(sprite);\n        }\n      }\n      {\n        const futureIntersection = timeIntervalIntersection(futureWindow, interval);\n        if (futureIntersection[0] < futureIntersection[1]) {\n          const [sprite] = this.spritePool.allocate(\"futureTimeInterval\" + i);\n          // const sprite = new Sprite();\n          sprite.width = ((futureIntersection[1] - futureIntersection[0]) / (this.tracker.timeWindow * 2)) * WIDTH;\n          sprite.height = KEY_HEIGHT;\n          sprite.position.set(msToPx(futureIntersection[0]), 0);\n          sprite.alpha = 0.1;\n          sprite.tint = this.tint;\n          this.container.addChild(sprite);\n        }\n      }\n    }\n    this.spritePool.releaseUntouched();\n  }\n}\n\n// As a reusable component\n// interface KeyPressOverlaySettings {}\n\nconst MIN_WINDOW_DURATION = 100;\nconst MAX_WINDOW_DURATION = 2000;\nconst DEFAULT_WINDOW_DURATION = 500;\nconst DEFAULT_DEBUG_RECTANGLE_ALPHA = 0.5;\n\n// [time - duration, time + duration][\nfunction createTimeWindow(time: number, duration: number): TimeInterval {\n  return [time - duration, time + duration];\n}\n\nfunction hitObjectWindow(hitObject: MainHitObject): TimeInterval {\n  if (isHitCircle(hitObject)) return [hitObject.hitTime, hitObject.hitTime];\n  return [hitObject.startTime, hitObject.startTime + hitObject.duration];\n}\n\nfunction preventBrowserShortcuts(e: KeyboardEvent) {\n  if (e.key === \"Alt\") {\n    // console.log(\"Prevented Alt\");\n    e.preventDefault();\n  }\n}\n\nexport class KeyPressOverlay {\n  public container: Container;\n  private key1: KeyPressOverlayRow;\n  private key2: KeyPressOverlayRow;\n  private hitObjectContainer: Container;\n  private rulerTicksContainer: Container;\n  private spritePool: TemporaryObjectPool<Sprite>;\n\n  private hitObjects: OsuHitObject[] = [];\n\n  // Determines how much in the past and how much in the future is visible, i.e., the visible window shows\n  // `[currentTime - windowDuration, currentTime + windowDuration]`\n  private windowDuration: number = DEFAULT_WINDOW_DURATION;\n  private hitObjectTracker: NonIntersectingTimeIntervalsTracker = new NonIntersectingTimeIntervalsTracker([], 0);\n  private timeIntervals: TimeInterval[] = [];\n\n  // Currently the rectangle is still used for debugging purposes ... will be replaced by something fancier in the\n  // future.\n  private debugRectangle = new Graphics();\n  private hovered = false;\n\n  constructor(private readonly gameplayClock: GameplayClock) {\n    this.container = new Container();\n    this.container.width = WIDTH;\n    this.container.height = HEIGHT;\n\n    // Just for debugging\n    {\n      this.debugRectangle.lineStyle(2, 0xffffff);\n      this.debugRectangle.drawRect(0, 0, WIDTH, HEIGHT);\n      this.debugRectangle.alpha = DEFAULT_DEBUG_RECTANGLE_ALPHA;\n      this.container.addChild(this.debugRectangle);\n    }\n\n    this.key1 = new KeyPressOverlayRow([], DEFAULT_WINDOW_DURATION);\n    this.key2 = new KeyPressOverlayRow([], DEFAULT_WINDOW_DURATION);\n    this.key1.onTintChange(DEFAULT_ANALYSIS_CURSOR_SETTINGS.colorKey1);\n    this.key2.onTintChange(DEFAULT_ANALYSIS_CURSOR_SETTINGS.colorKey2);\n    this.hitObjectContainer = new Container();\n    this.rulerTicksContainer = new Container();\n    this.container.addChild(\n      this.key1.container,\n      this.key2.container,\n      this.hitObjectContainer,\n      this.rulerTicksContainer,\n    );\n    // const margin = 10;\n    this.key1.container.position.set(0, HEIGHT / 2 - KEY_HEIGHT);\n    this.key2.container.position.set(0, HEIGHT / 2);\n\n    {\n      const middleLine = new Sprite(Texture.WHITE);\n      middleLine.width = 1;\n      middleLine.height = HEIGHT;\n      middleLine.alpha = 0.4;\n      middleLine.position.set(WIDTH / 2, 0);\n      this.container.addChild(middleLine);\n    }\n\n    this.setupRulerTicks();\n\n    this.spritePool = new TemporaryObjectPool<Sprite>(\n      () => new Sprite(Texture.WHITE),\n      (g) => {\n        // unexpected arrow function\n      },\n      { initialSize: 50 },\n    );\n\n    // https://jsfiddle.net/funkybjorn/ccqz6n8a/3/\n    this.container.interactive = true;\n    // this.container.buttonMode = true;\n    this.container.on(\"mouseover\", this.onMouseOver.bind(this));\n    this.container.on(\"mouseout\", this.onMouseOut.bind(this));\n    // We need this hitArea otherwise we can only zoom when hovering over a child, which is awkward\n    this.container.hitArea = new Rectangle(0, 0, WIDTH, HEIGHT);\n\n    window.addEventListener(\"wheel\", this.onWheelEvent.bind(this));\n  }\n\n  onWheelEvent(ev: WheelEvent) {\n    if (!this.hovered) {\n      return;\n    }\n    // It's something like 100 or 200\n    // console.log(\"wheel deltaY=\", ev.deltaY);\n\n    // When holding the alt key, we can zoom\n    if (ev.altKey) {\n      const scaling = 0.5;\n      const newWindowDuration = clamp(\n        ev.deltaY * scaling + this.windowDuration,\n        MIN_WINDOW_DURATION,\n        MAX_WINDOW_DURATION,\n      );\n      this.onWindowDurationChange(newWindowDuration);\n    } else {\n      // Currently this is a very dumb way of scrolling\n      // In the future we should make the scrolling work like in the editor where it snaps to the note / beat snap\n      // divisor.\n      const scaling = 0.1;\n      const newTime = clamp(\n        ev.deltaY * scaling + this.gameplayClock.timeElapsedInMs,\n        0,\n        this.gameplayClock.durationInMs,\n      );\n      this.gameplayClock.seekTo(newTime);\n    }\n  }\n\n  onMouseOver() {\n    this.hovered = true;\n    this.debugRectangle.alpha = 1;\n\n    // Otherwise the browser menu will be opened, which is pretty annoying\n    window.addEventListener(\"keydown\", preventBrowserShortcuts);\n  }\n\n  onMouseOut() {\n    this.hovered = false;\n    this.debugRectangle.alpha = DEFAULT_DEBUG_RECTANGLE_ALPHA;\n\n    window.removeEventListener(\"keydown\", preventBrowserShortcuts);\n  }\n\n  onWindowDurationChange(windowDuration: number) {\n    this.windowDuration = windowDuration;\n    this.key1.tracker.timeWindow = windowDuration;\n    this.key2.tracker.timeWindow = windowDuration;\n    this.hitObjectTracker.timeWindow = windowDuration;\n    this.setupRulerTicks();\n  }\n\n  onKeyPressesChange(timeIntervals: TimeIntervals[]) {\n    this.key1.tracker.intervals = timeIntervals[0];\n    this.key2.tracker.intervals = timeIntervals[1];\n  }\n\n  onHitObjectsChange(hitObjects: OsuHitObject[]) {\n    this.hitObjects = hitObjects;\n    this.timeIntervals = this.hitObjects.map(hitObjectWindow);\n    this.hitObjectTracker = new NonIntersectingTimeIntervalsTracker(this.timeIntervals, this.windowDuration);\n  }\n\n  setupRulerTicks() {\n    const tickHeight = 5,\n      tickWidth = 1;\n    const tickAlpha = 0.2;\n\n    this.rulerTicksContainer.removeChildren();\n    for (let i = 5; i <= 100; i += 5) {\n      // Bottom\n      {\n        const tick = new Sprite(Texture.WHITE);\n        tick.width = tickWidth;\n        tick.height = i % 25 === 0 ? tickHeight * 2 : tickHeight;\n        tick.alpha = tickAlpha;\n        tick.position.set(positionInTimeline(0, this.windowDuration, -i), HEIGHT - tick.height);\n        this.rulerTicksContainer.addChild(tick);\n      }\n\n      // Top\n      {\n        const tick = new Sprite(Texture.WHITE);\n        tick.width = tickWidth;\n        tick.height = i % 25 === 0 ? tickHeight * 2 : tickHeight;\n        tick.alpha = tickAlpha;\n        tick.position.set(positionInTimeline(0, this.windowDuration, -i), 0);\n        this.rulerTicksContainer.addChild(tick);\n      }\n    }\n  }\n\n  update(currentTime: number) {\n    this.key1.update(currentTime);\n    this.key2.update(currentTime);\n\n    // Check which objects are visible and draw them accordingly\n    const window = createTimeWindow(currentTime, this.windowDuration);\n    const indices = this.hitObjectTracker.findVisibleIndices(currentTime);\n\n    // console.log(`Found ${indices.length} hitobjects to draw!`);\n    // Show from back to forth\n    this.hitObjectContainer.removeChildren();\n    for (let i = indices.length - 1; i >= 0; i--) {\n      const hitObject = this.hitObjects[indices[i]];\n\n      // He\n      const aHeight = HEIGHT * 0.35;\n      let sprite;\n      if (isHitCircle(hitObject)) {\n        [sprite] = this.spritePool.allocate(\"hitCircle\" + indices[i]);\n        sprite.width = 5;\n        sprite.height = aHeight;\n\n        const x = positionInTimeline(currentTime, this.windowDuration, hitObject.hitTime);\n        sprite.position.set(x, (HEIGHT - sprite.height) / 2);\n        // console.log(x);\n        this.hitObjectContainer.addChild(sprite);\n        // this.hitObjectContainer.addChild(area.container);\n      } else {\n        [sprite] = this.spritePool.allocate(\"durationBox\" + indices[i]);\n        const intersection = timeIntervalIntersection(this.timeIntervals[indices[i]], window);\n        const duration = intersection[1] - intersection[0];\n        sprite.width = (duration / (2 * this.windowDuration)) * WIDTH;\n        sprite.height = aHeight;\n        const x = positionInTimeline(currentTime, this.windowDuration, intersection[0]);\n        sprite.position.set(x, (HEIGHT - sprite.height) / 2);\n      }\n      sprite.alpha = 0.727;\n      this.hitObjectContainer.addChild(sprite);\n    }\n    this.spritePool.releaseUntouched();\n  }\n}\n\n@injectable()\nexport class KeyPressWithNoteSheetPreparer {\n  keyPressOverlay: KeyPressOverlay;\n\n  constructor(\n    private readonly gameplayClock: GameplayClock,\n    private readonly replayManager: ReplayManager,\n    private readonly beatmapManager: BeatmapManager,\n  ) {\n    this.keyPressOverlay = new KeyPressOverlay(gameplayClock);\n\n    this.replayManager.mainReplay$.subscribe((replay) => {\n      if (replay === null) {\n        console.debug(\"Key presses have been reset\");\n        this.keyPressOverlay.onKeyPressesChange([[], []]);\n      } else {\n        const clicks = calculateReplayClicks(replay.frames);\n        console.debug(`KeyPressOverlay: Calculated clicks: [${clicks[0].length}, ${clicks[1].length}]`);\n        this.keyPressOverlay.onKeyPressesChange(clicks);\n      }\n    });\n    this.beatmapManager.beatmap$.subscribe((beatmap) => {\n      this.keyPressOverlay.onHitObjectsChange(beatmap.hitObjects);\n    });\n  }\n\n  get container() {\n    return this.keyPressOverlay.container;\n  }\n\n  update() {\n    const time = this.gameplayClock.timeElapsedInMs;\n    this.keyPressOverlay.update(time);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/CursorPreparer.ts",
    "content": "import { Container } from \"pixi.js\";\nimport { OsuClassicCursor } from \"@rewind/osu-pixi/classic-components\";\nimport { findIndexInReplayAtTime, interpolateReplayPosition } from \"../../../../utils/replay\";\nimport { AnalysisCursor, AnalysisPoint } from \"@rewind/osu-pixi/rewind\";\nimport { OsuAction } from \"@osujs/core\";\nimport { injectable } from \"inversify\";\nimport { GameplayClock } from \"../../../common/game/GameplayClock\";\nimport { ReplayManager } from \"../../../manager/ReplayManager\";\nimport { AnalysisCursorSettingsStore } from \"../../../analysis/analysis-cursor\";\nimport { ReplayCursorSettingsStore } from \"../../../common/replay-cursor\";\nimport { Position } from \"@osujs/math\";\nimport { SkinHolder } from \"../../../common/skin\";\n\n@injectable()\nexport class CursorPreparer {\n  private readonly container: Container;\n  private readonly osuClassicCursor: OsuClassicCursor;\n  private readonly analysisCursor: AnalysisCursor;\n\n  constructor(\n    private readonly replayManager: ReplayManager,\n    private readonly skinManager: SkinHolder,\n    private readonly gameClock: GameplayClock,\n    private readonly analysisCursorSettingsStore: AnalysisCursorSettingsStore,\n    private readonly replayCursorSettingsStore: ReplayCursorSettingsStore,\n  ) {\n    this.container = new Container();\n    this.osuClassicCursor = new OsuClassicCursor();\n    this.analysisCursor = new AnalysisCursor();\n  }\n\n  get time() {\n    return this.gameClock.timeElapsedInMs;\n  }\n\n  get analysisCursorSettings() {\n    return this.analysisCursorSettingsStore.settings;\n  }\n\n  updateOsuCursor() {\n    const { enabled, scaleWithCS, scale, showTrail, smoothCursorTrail } = this.replayCursorSettingsStore.settings;\n\n    if (!enabled) {\n      return;\n    }\n\n    const skin = this.skinManager.getSkin();\n    const replay = this.replayManager.getMainReplay();\n    const { time } = this;\n    const cursor = this.osuClassicCursor;\n    if (!replay) return;\n    const frames = replay.frames;\n\n    const pi = findIndexInReplayAtTime(frames, time);\n    // we are either before the beginning or after the end of the replay\n    if (pi === -1 || pi + 1 >= frames.length) return;\n\n    // console.log(pi);\n    const position = interpolateReplayPosition(frames[pi], frames[pi + 1], time);\n\n    const trailPositions: Position[] = [];\n    const numberOfTrailSprites = 8;\n    if (showTrail) {\n      if (smoothCursorTrail) {\n        const fps = 144;\n        const deltaCursorSmoothingTime = Math.floor(1000 / fps);\n        let t = time - deltaCursorSmoothingTime,\n          j = pi + 1;\n        while (j > 0 && trailPositions.length < numberOfTrailSprites) {\n          while (j > 0 && frames[j - 1].time > t) {\n            j--;\n          }\n          if (j === 0) {\n            trailPositions.push(frames[j].position);\n          } else {\n            trailPositions.push(interpolateReplayPosition(frames[j - 1], frames[j], t));\n          }\n          t -= deltaCursorSmoothingTime;\n        }\n      } else {\n        for (let i = 1; i <= numberOfTrailSprites && pi - i >= 0; i++) {\n          trailPositions.push(frames[pi - i].position);\n        }\n      }\n    }\n\n    cursor.prepare({\n      position,\n      trailPositions,\n      cursorScale: scale,\n      cursorTexture: skin.getTexture(\"CURSOR\"),\n      cursorTrailTexture: skin.getTexture(\"CURSOR_TRAIL\"),\n    });\n\n    this.container.addChild(cursor.container);\n  }\n\n  get replay() {\n    return this.replayManager.getMainReplay();\n  }\n\n  updateAnalysisCursor() {\n    const { replay, time, analysisCursorSettings } = this;\n    const { enabled } = analysisCursorSettings;\n    if (!enabled) {\n      return;\n    }\n\n    if (!replay) return;\n    const { frames } = replay;\n    const cursor = this.analysisCursor;\n    const pi = findIndexInReplayAtTime(frames, time);\n    // we are either before the beginning or after the end of the replay\n    if (pi === -1 || pi + 1 >= frames.length) return;\n    const position = interpolateReplayPosition(frames[pi], frames[pi + 1], time);\n\n    const interesting: boolean[] = [];\n    const masks: number[] = [];\n    // the interesting rule can be changed...\n\n    const points: AnalysisPoint[] = [];\n    const trailCount = 25;\n    for (let i = trailCount - 1; i >= 0; i--) {\n      masks[i] = 0;\n      if (pi - i >= 0) {\n        const r = frames[pi - i];\n        if (r.actions.includes(OsuAction.leftButton)) masks[i] |= 1;\n        if (r.actions.includes(OsuAction.rightButton)) masks[i] |= 2;\n        if (i + 1 === trailCount) continue;\n\n        // i has a bit that i + 1 does not have (bit=press)\n        if ((masks[i] ^ masks[i + 1]) & masks[i]) {\n          interesting[i] = true;\n        }\n      }\n    }\n    const colorScheme = [\n      analysisCursorSettings.colorNoKeys,\n      analysisCursorSettings.colorKey1,\n      analysisCursorSettings.colorKey2,\n      analysisCursorSettings.colorBothKeys,\n    ];\n    for (let i = 0; i < trailCount && pi - i >= 0; i++) {\n      const j = pi - i;\n      const maskDelta = (masks[i] ^ masks[i + 1]) & masks[i]; //\n      points.push({\n        interesting: interesting[i],\n        color: colorScheme[maskDelta],\n        position: frames[j].position,\n      });\n    }\n    cursor.prepare({ points, smoothedPosition: position });\n    cursor.container.position.set(position.x, position.y);\n\n    this.container.addChild(cursor.container);\n  }\n\n  updateCursors() {\n    this.container.removeChildren();\n    this.updateOsuCursor();\n    this.updateAnalysisCursor();\n  }\n\n  getContainer() {\n    return this.container;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/HitCircleFactory.ts",
    "content": "import { injectable } from \"inversify\";\nimport { HitCircle } from \"@osujs/core\";\nimport { GameplayClock } from \"../../../common/game/GameplayClock\";\nimport {\n  HitResult,\n  OsuClassicApproachCircle,\n  OsuClassicApproachCircleSettings,\n  OsuClassicHitCircleArea,\n  OsuClassicHitCircleAreaSettings,\n} from \"@rewind/osu-pixi/classic-components\";\nimport type { ISkin } from \"../../../../model/Skin\";\nimport { GameSimulator } from \"../../../common/game/GameSimulator\";\nimport { TemporaryObjectPool } from \"../../../../utils/pooling/TemporaryObjectPool\";\nimport { ModSettingsService } from \"../../../analysis/mod-settings\";\nimport { SkinHolder } from \"../../../common/skin\";\n\n// TODO: Maybe it's even dynamic\nconst HIT_CIRCLE_FADE_OUT_DURATION = 300;\n\n/**\n * Connected to all the different components and knows how to create the corresponding hit circle components (area +\n * approach circle).\n */\n@injectable()\nexport class HitCircleFactory {\n  hitAreaPool: TemporaryObjectPool<OsuClassicHitCircleArea>;\n\n  constructor(\n    private readonly gameClock: GameplayClock,\n    private readonly gameSimulator: GameSimulator,\n    private readonly modSettingsService: ModSettingsService,\n    private readonly stageSkinService: SkinHolder,\n  ) {\n    this.hitAreaPool = new TemporaryObjectPool<OsuClassicHitCircleArea>(\n      () => new OsuClassicHitCircleArea(),\n      (t) => {},\n      { initialSize: 50 },\n    );\n  }\n\n  // TODO: Pooling\n  private getOsuClassicHitCircleArea(id: string) {\n    return new OsuClassicHitCircleArea();\n  }\n\n  private getOsuClassicApproachCircle(id: string) {\n    return new OsuClassicApproachCircle({});\n  }\n\n  freeResources() {\n    this.hitAreaPool.releaseUntouched();\n  }\n\n  createHitCircle(hitCircle: HitCircle) {\n    const time = this.gameClock.timeElapsedInMs;\n    const modSettings = this.modSettingsService.modSettings;\n    const skin = this.stageSkinService.getSkin();\n\n    const modHidden = modSettings.hidden;\n\n    const isVisible = hitCircle.spawnTime <= time && time <= hitCircle.hitTime + HIT_CIRCLE_FADE_OUT_DURATION;\n\n    if (!isVisible) return undefined;\n\n    const gameplayState = this.gameSimulator.getCurrentState();\n\n    const area = this.getOsuClassicHitCircleArea(hitCircle.id);\n    // const [area] = this.hitAreaPool.allocate(hitCircle.id);\n    // TODO: Replace this with a service that can also be mocked and simulate AUTO play\n    const hitCircleState = gameplayState?.hitCircleVerdict[hitCircle.id];\n\n    const hitResult = hitCircleState\n      ? {\n          hit: hitCircleState.type !== \"MISS\",\n          timing: hitCircleState.judgementTime - hitCircle.hitTime,\n        }\n      : null;\n    area.prepare(createHitCircleAreaSettings({ hitCircle, gameTime: time, modHidden, skin, hitResult }));\n\n    const approachCircle = this.getOsuClassicApproachCircle(hitCircle.id);\n    approachCircle.prepare(createApproachCircleSettings({ hitCircle, skin, gameTime: time, modHidden }));\n\n    return {\n      hitCircleArea: area.container,\n      approachCircle: approachCircle.sprite,\n    };\n  }\n}\n\nexport function createApproachCircleSettings(s: {\n  hitCircle: HitCircle;\n  skin: ISkin;\n  gameTime: number;\n  modHidden?: boolean;\n}): Partial<OsuClassicApproachCircleSettings> {\n  const { hitCircle, skin, gameTime, modHidden } = s;\n  return {\n    time: gameTime - hitCircle.hitTime,\n    texture: skin.getTexture(\"APPROACH_CIRCLE\"),\n    approachDuration: hitCircle.approachDuration,\n    scale: hitCircle.scale,\n    position: hitCircle.position,\n    tint: skin.getComboColorForIndex(hitCircle.comboSetIndex),\n    modHidden: modHidden,\n    // fadeInDuration, numberScaling\n  };\n}\n\nfunction createHitCircleAreaSettings(s: {\n  hitCircle: HitCircle;\n  skin: ISkin;\n  gameTime: number;\n  modHidden?: boolean;\n  hitResult: HitResult | null;\n}): Partial<OsuClassicHitCircleAreaSettings> {\n  const { gameTime, hitCircle, skin, modHidden, hitResult } = s;\n  return {\n    time: gameTime - hitCircle.hitTime,\n    numberTextures: skin.getHitCircleNumberTextures(),\n    numberOverlap: skin.config.fonts.hitCircleOverlap,\n    hitCircleOverlayAboveNumber: skin.config.general.hitCircleOverlayAboveNumber,\n\n    hitCircleTexture: skin.getTexture(\"HIT_CIRCLE\"),\n    hitCircleOverlayTexture: skin.getTexture(\"HIT_CIRCLE_OVERLAY\"),\n\n    tint: skin.getComboColorForIndex(hitCircle.comboSetIndex),\n\n    number: hitCircle.withinComboSetIndex + 1,\n    approachDuration: hitCircle.approachDuration,\n\n    modHidden,\n    scale: hitCircle.scale,\n    position: hitCircle.position,\n    hitResult,\n    // fadeInDuration, numberScaling\n  };\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/HitObjectsContainerFactory.ts",
    "content": "import { Container } from \"pixi.js\";\nimport { injectable } from \"inversify\";\nimport { HitCircle, isHitCircle, isSlider, Slider, Spinner } from \"@osujs/core\";\nimport { HitCircleFactory } from \"./HitCircleFactory\";\nimport { SliderFactory } from \"./SliderFactory\";\nimport { SpinnerFactory } from \"./SpinnerFactory\";\nimport { BeatmapManager } from \"../../../manager/BeatmapManager\";\n\nexport class HitObjectsContainer {\n  approachCircleContainer: Container;\n  spinnerProxies: Container;\n  hitObjectContainer: Container;\n\n  constructor(\n    private readonly beatmapManager: BeatmapManager,\n    private readonly hitCircleFactory: HitCircleFactory,\n    private readonly sliderFactory: SliderFactory,\n    private readonly spinnerFactory: SpinnerFactory,\n  ) {\n    this.approachCircleContainer = new Container();\n    this.hitObjectContainer = new Container();\n    this.spinnerProxies = new Container();\n  }\n\n  private createHitCircle(hitCircle: HitCircle) {\n    const { hitCircleArea, approachCircle } = this.hitCircleFactory.createHitCircle(hitCircle) ?? {};\n    if (hitCircleArea) this.hitObjectContainer.addChild(hitCircleArea);\n    if (approachCircle) this.approachCircleContainer.addChild(approachCircle);\n  }\n\n  private createSlider(slider: Slider) {\n    const body = this.sliderFactory.create(slider);\n    if (body) this.hitObjectContainer.addChild(body);\n    this.createHitCircle(slider.head);\n  }\n\n  private createSpinner(spinner: Spinner) {\n    const spinnerGraphic = this.spinnerFactory.create(spinner);\n    if (spinnerGraphic) this.spinnerProxies.addChild(spinnerGraphic.container);\n  }\n\n  updateHitObjects() {\n    this.hitObjectContainer.removeChildren();\n    this.spinnerProxies.removeChildren();\n    this.approachCircleContainer.removeChildren();\n\n    const { hitObjects } = this.beatmapManager.getBeatmap();\n\n    // TODO: This assumes that they are ordered by some time\n    for (let i = hitObjects.length - 1; i >= 0; i--) {\n      const hitObject = hitObjects[i];\n      if (isHitCircle(hitObject)) {\n        this.createHitCircle(hitObject);\n      } else if (isSlider(hitObject)) {\n        this.createSlider(hitObject);\n      } else {\n        this.createSpinner(hitObject);\n      }\n    }\n\n    this.hitCircleFactory.freeResources();\n    this.sliderFactory.postUpdate();\n  }\n}\n\n@injectable()\nexport class HitObjectsContainerFactory {\n  constructor(\n    private readonly beatmapManager: BeatmapManager,\n    private readonly hitCirclePreparer: HitCircleFactory,\n    private readonly sliderPreparer: SliderFactory,\n    private readonly spinnerPreparer: SpinnerFactory,\n  ) {}\n\n  createHitObjectsContainer() {\n    return new HitObjectsContainer(\n      this.beatmapManager,\n      this.hitCirclePreparer,\n      this.sliderPreparer,\n      this.spinnerPreparer,\n    );\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/JudgementPreparer.ts",
    "content": "import { injectable } from \"inversify\";\nimport { Container } from \"pixi.js\";\nimport { OsuClassicJudgement } from \"@rewind/osu-pixi/classic-components\";\nimport { circleSizeToScale } from \"@osujs/math\";\nimport { MainHitObjectVerdict } from \"@osujs/core\";\nimport { GameplayClock } from \"../../../common/game/GameplayClock\";\nimport { GameSimulator } from \"../../../common/game/GameSimulator\";\nimport { BeatmapManager } from \"../../../manager/BeatmapManager\";\nimport { SkinHolder } from \"../../../common/skin\";\n\nfunction texturesForJudgement(t: MainHitObjectVerdict, lastInComboSet?: boolean) {\n  switch (t) {\n    case \"GREAT\":\n      return lastInComboSet ? \"HIT_300K\" : \"HIT_300\";\n    case \"OK\":\n      return lastInComboSet ? \"HIT_100K\" : \"HIT_100\";\n    case \"MEH\":\n      return \"HIT_50\";\n    case \"MISS\":\n      return \"HIT_0\";\n  }\n}\n\n@injectable()\nexport class JudgementPreparer {\n  private readonly container: Container;\n\n  constructor(\n    private readonly gameClock: GameplayClock,\n    private readonly stageSkinService: SkinHolder,\n    private readonly beatmapManager: BeatmapManager,\n    private readonly gameSimulator: GameSimulator,\n  ) {\n    this.container = new Container();\n  }\n\n  getContainer() {\n    return this.container;\n  }\n\n  updateJudgements() {\n    this.container.removeChildren();\n    const beatmap = this.beatmapManager.getBeatmap();\n    const time = this.gameClock.timeElapsedInMs;\n    const skin = this.stageSkinService.getSkin();\n    const judgements = this.gameSimulator.judgements;\n    // TODO: Order might not be correct\n    for (const j of judgements) {\n      const timeAgo = time - j.time;\n      if (!(timeAgo >= 0 && timeAgo < 3000) || j.verdict === \"GREAT\") continue;\n\n      const lastInComboSet = false;\n      const textures = skin.getTextures(texturesForJudgement(j.verdict, lastInComboSet));\n      const animationFrameRate = skin.config.general.animationFrameRate;\n      const judgement = new OsuClassicJudgement();\n      const scale = circleSizeToScale(beatmap.difficulty.circleSize);\n\n      // TODO: Should be configurable, technically speaking sliderHeadJudgementSkip=false does not reflect osu!stable\n      // (it resembles lazer) However, in this replay analysis tool this is more useful (?)\n      const sliderHeadJudgementSkip = true;\n      if (sliderHeadJudgementSkip && j.isSliderHead) continue;\n      judgement.prepare({ time: timeAgo, position: j.position, scale, animationFrameRate, textures });\n      // judgement.sprite.zIndex = -timeAgo;\n      this.container.addChild(judgement.sprite);\n    }\n    // this.judgementLayer.sortChildren();\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/PlayfieldBorderFactory.ts",
    "content": "import { injectable } from \"inversify\";\nimport { PlayfieldBorder } from \"@rewind/osu-pixi/classic-components\";\nimport { PlayfieldBorderSettingsStore } from \"../../../common/playfield-border\";\n\n/**\n * Creates a `PlayfieldBorder` that is connected to the settings store and can adjust its style, thickness based\n * on user input.\n */\n@injectable()\nexport class PlayfieldBorderFactory {\n  constructor(private settingsStore: PlayfieldBorderSettingsStore) {}\n\n  createPlayfieldBorder() {\n    const playfieldBorder = new PlayfieldBorder();\n    // Maybe also subscribes to CS changes\n    this.settingsStore.settings$.subscribe((s) => playfieldBorder.onSettingsChange(s));\n    return playfieldBorder;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/PlayfieldFactory.ts",
    "content": "import * as PIXI from \"pixi.js\";\nimport { PlayfieldBorderFactory } from \"./PlayfieldBorderFactory\";\nimport { injectable } from \"inversify\";\nimport { HitObjectsContainer, HitObjectsContainerFactory } from \"./HitObjectsContainerFactory\";\nimport { CursorPreparer } from \"./CursorPreparer\";\nimport { JudgementPreparer } from \"./JudgementPreparer\";\nimport { PlayfieldBorder } from \"@rewind/osu-pixi/classic-components\";\n\nexport class Playfield {\n  public readonly container: PIXI.Container;\n\n  constructor(\n    private readonly playfieldBorder: PlayfieldBorder,\n    private readonly hitObjectsContainer: HitObjectsContainer,\n    private cursorPreparer: CursorPreparer,\n    private judgementPreparer: JudgementPreparer,\n  ) {\n    this.container = new PIXI.Container();\n    this.container.addChild(\n      this.playfieldBorder.graphics,\n      this.hitObjectsContainer.spinnerProxies,\n      this.judgementPreparer.getContainer(),\n      this.hitObjectsContainer.hitObjectContainer,\n      this.hitObjectsContainer.approachCircleContainer,\n      this.cursorPreparer.getContainer(),\n    );\n  }\n\n  updatePlayfield() {\n    // playfieldBorder is push-based, therefore no .update()\n    this.hitObjectsContainer.updateHitObjects();\n    this.cursorPreparer.updateCursors();\n    this.judgementPreparer.updateJudgements();\n  }\n}\n\n@injectable()\nexport class PlayfieldFactory {\n  constructor(\n    private playfieldBorderFactory: PlayfieldBorderFactory,\n    private hitObjectsContainerFactory: HitObjectsContainerFactory,\n    private cursorPreparer: CursorPreparer,\n    private judgementPreparer: JudgementPreparer,\n  ) {}\n\n  createPlayfield() {\n    return new Playfield(\n      this.playfieldBorderFactory.createPlayfieldBorder(),\n      this.hitObjectsContainerFactory.createHitObjectsContainer(),\n      this.cursorPreparer,\n      this.judgementPreparer,\n    );\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/SliderFactory.ts",
    "content": "import { Skin } from \"../../../../model/Skin\";\nimport { Slider, SliderCheckPoint } from \"@osujs/core\";\nimport { Container, DisplayObject, Graphics, Sprite, Texture } from \"pixi.js\";\nimport {\n  OsuClassicSliderBall,\n  OsuClassicSliderBody,\n  OsuClassicSliderRepeat,\n  OsuClassicSliderTick,\n  SliderBodySettings,\n} from \"@rewind/osu-pixi/classic-components\";\nimport { RGB, sliderRepeatAngle, Vec2 } from \"@osujs/math\";\nimport { GameplayClock } from \"../../../common/game/GameplayClock\";\nimport { injectable } from \"inversify\";\nimport { TemporaryObjectPool } from \"../../../../utils/pooling/TemporaryObjectPool\";\nimport { SliderTextureManager } from \"../sliders/SliderTextureManager\";\nimport { GameSimulator } from \"../../../common/game/GameSimulator\";\nimport { BeatmapRenderService } from \"../../../common/beatmap-render\";\nimport { ModSettingsService } from \"../../../analysis/mod-settings\";\nimport { SkinHolder } from \"../../../common/skin\";\n\nconst DEBUG_FOLLOW_CIRCLE_COLOR = 0xff0000;\nconst DEBUG_PIXEL_BALL_COLOR = 0x00ff00;\n\nconst SLIDER_FADE_OUT_TIME = 300;\n\n@injectable()\nexport class SliderFactory {\n  graphicsPool: TemporaryObjectPool<Graphics>;\n\n  constructor(\n    private readonly gameClock: GameplayClock,\n    private readonly gameSimulator: GameSimulator,\n    private readonly modSettingsService: ModSettingsService,\n    private readonly beatmapRenderSettingsStore: BeatmapRenderService,\n    private readonly stageSkinService: SkinHolder,\n    private readonly sliderTextureService: SliderTextureManager,\n  ) {\n    // TODO: Inject\n    this.graphicsPool = new TemporaryObjectPool<Graphics>(\n      () => new Graphics(),\n      (g) => g.clear(),\n      { initialSize: 10 },\n    );\n  }\n\n  private getSliderBody(id: string) {\n    return new OsuClassicSliderBody();\n  }\n\n  private get modHidden() {\n    return this.modSettingsService.modSettings.hidden;\n  }\n\n  private prepareSliderBody(slider: Slider) {\n    const { skin, time: gameTime, modHidden } = this;\n    const texture = this.sliderTextureService.getTexture({\n      id: slider.id,\n      points: slider.path.calculatedPath,\n      resolution: 2.25, // TODO: Dynamic\n      radius: slider.radius,\n      borderColor: skin.config.colors.sliderBorder as RGB,\n    });\n    const setting = sliderBodySetting({ gameTime, skin, modHidden, slider, texture });\n    const body = this.getSliderBody(slider.id);\n    body.prepare(setting);\n    return body.container;\n  }\n\n  private prepareSliderTail(slider: Slider) {\n    return new Container();\n  }\n\n  // Only for DEBUG option\n  private prepareSliderLastLegacyTick(checkpoint: SliderCheckPoint) {\n    const delta = checkpoint.hitTime - this.time;\n    if (!(delta >= 0 && delta < 500)) return undefined;\n    const [lastTick] = this.graphicsPool.allocate(checkpoint.id + \"/raw\");\n    lastTick.clear();\n    lastTick.beginFill(0xff0000);\n    lastTick.drawCircle(checkpoint.position.x, checkpoint.position.y, 2);\n    lastTick.endFill();\n    return lastTick;\n  }\n\n  private prepareSliderTicks(ticks: SliderCheckPoint[]) {\n    const { time: gameTime, skin } = this;\n    const sprites: Sprite[] = [];\n    ticks.forEach((t) => {\n      // See SliderTick.cs\n      const { spanIndex, spanStartTime, position, scale } = t;\n      const offset = spanIndex > 0 ? 200 : 400 * 0.66;\n      const approachDuration = (t.hitTime - spanStartTime) / 2 + offset;\n      const graphic = new OsuClassicSliderTick();\n      const time = gameTime - t.hitTime;\n      const hit = true; // TODO\n      const texture = skin.getTexture(\"SLIDER_TICK\");\n      graphic.prepare({ time, position, scale, hit, approachDuration, texture });\n      sprites.push(graphic.sprite);\n    });\n    return sprites;\n  }\n\n  private prepareSliderRepeats(repeats: SliderCheckPoint[], slider: Slider) {\n    const { time: gameTime, skin } = this;\n    const first_end_circle_preempt_adjust = 2 / 3;\n    // This is a bit different if there is snaking\n    const rotationAngels = [false, true].map((b) => sliderRepeatAngle(slider.path.calculatedPath, b));\n    let isAtEnd = 1;\n    const sprites: Sprite[] = [];\n    repeats.forEach((r) => {\n      const { position, scale, hitTime } = r;\n      const approachDuration =\n        r.spanIndex > 0\n          ? slider.spanDuration * 2\n          : hitTime - slider.startTime + slider.head.approachDuration * first_end_circle_preempt_adjust;\n\n      const rotationAngle = rotationAngels[isAtEnd];\n      isAtEnd = 1 - isAtEnd;\n\n      if (gameTime < r.hitTime - approachDuration || r.hitTime + approachDuration < gameTime) return;\n      const repeat = new OsuClassicSliderRepeat();\n      const beatLength = 350;\n      // See DrawableSliderRepeat -> `animDuration`\n      const fadeInOutDuration = Math.min(300, slider.spanDuration);\n      const time = gameTime - hitTime;\n      const texture = skin.getTexture(\"SLIDER_REPEAT\");\n      repeat.prepare({\n        position,\n        scale,\n        time,\n        beatLength,\n        approachDuration,\n        texture,\n        fadeInOutDuration,\n        hit: true,\n        rotationAngle,\n      });\n      sprites.push(repeat.sprite);\n    });\n    return sprites;\n  }\n\n  private get sliderDevMode() {\n    return this.beatmapRenderSettingsStore.settings.sliderDevMode;\n  }\n\n  private prepareSliderBall(slider: Slider) {\n    const { time: gameTime, skin } = this;\n    if (gameTime < slider.startTime || slider.endTime < gameTime) return [];\n    let hit = true;\n\n    const currentState = this.gameSimulator.getCurrentState();\n    if (currentState) {\n      // In case the slider body state of slider.id has not been found, it means that tracking hasn't even started yet.\n      hit = currentState.sliderBodyState.get(slider.id)?.isTracking ?? false;\n    }\n\n    const sliderAnalysis = this.sliderDevMode;\n\n    const progress = (gameTime - slider.startTime) / slider.duration;\n    const position = slider.ballPositionAt(progress);\n    const displayObjects: DisplayObject[] = [];\n\n    const sliderBall = new OsuClassicSliderBall();\n    sliderBall.prepare({\n      ballTint: null, // TODO\n      ballTexture: skin.getTexture(\"SLIDER_BALL\"),\n      followCircleTexture: hit ? skin.getTexture(\"SLIDER_FOLLOW_CIRCLE\") : Texture.EMPTY,\n      position,\n      scale: slider.scale,\n    });\n    displayObjects.push(sliderBall.container);\n\n    if (sliderAnalysis) {\n      {\n        const [rawFollowCircle] = this.graphicsPool.allocate(slider.id + \"/rawFollowCircle\");\n        rawFollowCircle.clear();\n        rawFollowCircle.lineStyle(1, DEBUG_FOLLOW_CIRCLE_COLOR);\n        // TODO: Depending on if inside or not\n        rawFollowCircle.drawCircle(position.x, position.y, slider.radius * (hit ? 2.4 : 1));\n        displayObjects.push(rawFollowCircle);\n      }\n      {\n        const [pixelBall] = this.graphicsPool.allocate(slider.id + \"/pixelBall\");\n        pixelBall.clear();\n        pixelBall.beginFill(DEBUG_PIXEL_BALL_COLOR);\n        pixelBall.drawCircle(position.x, position.y, 1);\n        pixelBall.endFill();\n        displayObjects.push(pixelBall);\n      }\n    }\n    return displayObjects;\n  }\n\n  get time() {\n    return this.gameClock.timeElapsedInMs;\n  }\n\n  get skin() {\n    return this.stageSkinService.getSkin();\n  }\n\n  create(slider: Slider) {\n    const { time, sliderDevMode } = this;\n\n    const isVisible = slider.spawnTime <= time && time <= slider.endTime + SLIDER_FADE_OUT_TIME;\n    if (!isVisible) {\n      // VERY IMPORTANT, otherwise there will too many textures in the cache.\n      this.sliderTextureService.removeFromCache(slider.id);\n      return undefined;\n    }\n\n    const container = new Container();\n\n    const addChildren = (displayObjects: DisplayObject[]) =>\n      displayObjects.length > 0 ? container.addChild(...displayObjects) : undefined;\n\n    // Order: Body, Tail, Tick, Repeat, Ball, Head (?)\n    container.addChild(this.prepareSliderBody(slider));\n\n    const ticks: SliderCheckPoint[] = [];\n    const repeats: SliderCheckPoint[] = [];\n    let legacyTick;\n    slider.checkPoints.forEach((c) => {\n      if (c.type === \"TICK\") ticks.push(c);\n      if (c.type === \"REPEAT\") repeats.push(c);\n      if (c.type === \"LAST_LEGACY_TICK\") legacyTick = c; // can only be one\n    });\n\n    container.addChild(this.prepareSliderTail(slider));\n    addChildren(this.prepareSliderTicks(ticks));\n\n    if (legacyTick && sliderDevMode) {\n      const tick = this.prepareSliderLastLegacyTick(legacyTick);\n      if (tick) container.addChild(tick);\n    }\n    addChildren(this.prepareSliderRepeats(repeats, slider));\n    addChildren(this.prepareSliderBall(slider));\n    return container;\n  }\n\n  // TODO: ...\n  postUpdate() {\n    this.graphicsPool.releaseUntouched();\n  }\n}\n\nexport function sliderBodySetting(s: {\n  skin: Skin;\n  modHidden: boolean;\n  slider: Slider;\n  gameTime: number;\n  texture: Texture;\n}): SliderBodySettings {\n  const { texture, modHidden, slider, gameTime } = s;\n  const headPositionInRectangle = Vec2.scale(slider.path.boundaryBox[0], -1).add({\n    x: slider.radius,\n    y: slider.radius,\n  });\n\n  return {\n    modHidden,\n    duration: slider.duration,\n    position: slider.head.position,\n    approachDuration: slider.head.approachDuration,\n    time: gameTime - slider.startTime,\n    texture,\n    headPositionInRectangle,\n  };\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/playfield/SpinnerFactory.ts",
    "content": "import { injectable } from \"inversify\";\nimport { Spinner } from \"@osujs/core\";\nimport { GameplayClock } from \"../../../common/game/GameplayClock\";\nimport { OsuClassicSpinner } from \"@rewind/osu-pixi/classic-components\";\nimport { GameSimulator } from \"../../../common/game/GameSimulator\";\nimport { ModSettingsService } from \"../../../analysis/mod-settings\";\nimport { SkinHolder } from \"../../../common/skin\";\n\n// TODO: Maybe it's even dynamic\nconst SPINNER_FADE_OUT_DURATION = 300;\n\n@injectable()\nexport class SpinnerFactory {\n  constructor(\n    private readonly gameClock: GameplayClock,\n    private readonly gameSimulator: GameSimulator,\n    private readonly modSettingsService: ModSettingsService,\n    private readonly stageSkinService: SkinHolder,\n  ) {}\n\n  create(spinner: Spinner) {\n    const skin = this.stageSkinService.getSkin();\n    const time = this.gameClock.timeElapsedInMs;\n    const { hidden } = this.modSettingsService.modSettings;\n\n    if (spinner.startTime <= time && time <= spinner.endTime + SPINNER_FADE_OUT_DURATION) {\n      const gSpinner = new OsuClassicSpinner();\n      gSpinner.prepare({\n        approachCircleTexture: skin.getTexture(\"SPINNER_APPROACH_CIRCLE\"),\n        duration: spinner.duration,\n        time: time - spinner.endTime,\n        modHidden: hidden,\n      });\n\n      return gSpinner;\n    }\n    return undefined;\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/sliders/SliderTextureManager.ts",
    "content": "import { injectable } from \"inversify\";\nimport { PixiRendererManager } from \"../../PixiRendererManager\";\nimport { Position, RGB } from \"@osujs/math\";\nimport { BasicSliderTextureRenderer } from \"@rewind/osu-pixi/classic-components\";\nimport { Texture } from \"pixi.js\";\n\n// Sliders use sprites and they need the corresponding textures.\n// Usually here is where big optimizations can be made by batching the slider renderings and so on.\n// Slider textures use insane amount of (GPU) memory, that's why we need to dispose those which are not needed.\n// When there was no memory optimization, my GPU ran out of memory (4GB) after only 100 sliders (full screen\n// 1920x1080).\n\ntype SliderTextureJob = {\n  id: string;\n  points: Position[];\n  radius: number;\n  borderColor: RGB;\n  resolution: number;\n};\n\n@injectable()\nexport class SliderTextureManager {\n  textures: Map<string, Texture>;\n\n  constructor(private readonly pixiRendererService: PixiRendererManager) {\n    this.textures = new Map<string, Texture>();\n  }\n\n  getTexture(settings: SliderTextureJob): Texture {\n    const { id, points, radius, borderColor, resolution } = settings;\n    const texture = this.textures.get(id);\n    if (texture !== undefined) {\n      return texture;\n    }\n\n    const pixiRenderer = this.pixiRendererService.getRenderer();\n    if (pixiRenderer === undefined) {\n      throw Error(\"Could not render slider texture without a renderer in place.\");\n    }\n    const sliderTextureRenderer: BasicSliderTextureRenderer = new BasicSliderTextureRenderer(pixiRenderer);\n    const renderTexture = sliderTextureRenderer.render({\n      // The first point of .calculatedPath will be the head and also have the value (0, 0)\n      // The other points are relative positions to the head = (0, 0).\n      path: points,\n      radius,\n      resolution,\n      borderColor,\n    });\n    this.textures.set(id, renderTexture);\n    return renderTexture;\n    // We shift the sprite so that the pivot of this container correspond to the\n    // slider head of the texture.\n    // const { minX, minY } = getBoundsFromSliderPath(points, radius);\n    // sprite.position.set(minX, minY);\n  }\n\n  removeFromCache(id: string) {\n    const texture = this.textures.get(id);\n    if (texture !== undefined) {\n      texture.destroy();\n      this.textures.delete(id);\n    }\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/components/stage/AnalysisStage.ts",
    "content": "import * as PIXI from \"pixi.js\";\nimport { Container } from \"pixi.js\";\nimport { injectable } from \"inversify\";\nimport { Playfield, PlayfieldFactory } from \"../playfield/PlayfieldFactory\";\nimport { OSU_PLAYFIELD_HEIGHT, OSU_PLAYFIELD_WIDTH } from \"@osujs/core\";\nimport { ForegroundHUDPreparer } from \"../hud/ForegroundHUDPreparer\";\nimport { PixiRendererManager } from \"../../PixiRendererManager\";\nimport { BeatmapBackgroundFactory } from \"../background/BeatmapBackground\";\nimport { STAGE_HEIGHT, STAGE_WIDTH } from \"../../constants\";\nimport { KeyPressWithNoteSheetPreparer } from \"../keypresses/KeyPressOverlay\";\n\nconst ratio = STAGE_WIDTH / STAGE_HEIGHT;\nconst showNoteKeyOverlay = true;\n\n// This is a custom analysis stage that composes all the things it needs\n// Other stages for example do not want a foreground HUD and should therefore compose differently.\n@injectable()\nexport class AnalysisStage {\n  private screenWidth = 0;\n  private screenHeight = 0;\n\n  public stage: Container;\n  private playfield: Playfield;\n\n  constructor(\n    private rendererManager: PixiRendererManager,\n    private beatmapBackground: BeatmapBackgroundFactory,\n    private playfieldFactory: PlayfieldFactory,\n    private foregroundHUDPreparer: ForegroundHUDPreparer,\n    private keyPressOverlayPreparer: KeyPressWithNoteSheetPreparer,\n  ) {\n    const width = STAGE_WIDTH;\n    const height = STAGE_HEIGHT;\n    const background = beatmapBackground.createBeatmapBackground({ width, height });\n    this.playfield = playfieldFactory.createPlayfield();\n\n    this.stage = new PIXI.Container();\n    this.stage.addChild(\n      background.sprite,\n      this.playfield.container,\n      this.foregroundHUDPreparer.container,\n      this.keyPressOverlayPreparer.container,\n    );\n\n    // Optimization\n    this.playfield.container.interactiveChildren = false;\n\n    let playfieldScaling;\n    const overlayHeight = 0.1;\n\n    if (showNoteKeyOverlay) {\n      playfieldScaling = (STAGE_HEIGHT * 0.7) / OSU_PLAYFIELD_HEIGHT;\n      this.playfield.container.position.set(\n        (STAGE_WIDTH - OSU_PLAYFIELD_WIDTH * playfieldScaling) / 2,\n        (STAGE_HEIGHT - OSU_PLAYFIELD_HEIGHT * playfieldScaling) / 2 + OSU_PLAYFIELD_HEIGHT * overlayHeight,\n      );\n    } else {\n      playfieldScaling = (STAGE_HEIGHT * 0.8) / OSU_PLAYFIELD_HEIGHT;\n      this.playfield.container.position.set(\n        (STAGE_WIDTH - OSU_PLAYFIELD_WIDTH * playfieldScaling) / 2,\n        (STAGE_HEIGHT - OSU_PLAYFIELD_HEIGHT * playfieldScaling) / 2,\n      );\n    }\n\n    this.keyPressOverlayPreparer.container.position.set((STAGE_WIDTH - OSU_PLAYFIELD_WIDTH * playfieldScaling) / 2, 15);\n    this.keyPressOverlayPreparer.container.visible = showNoteKeyOverlay;\n\n    this.playfield.container.scale.set(playfieldScaling);\n  }\n\n  /**\n   *  So the virtual screen is supposed to have the dimensions 1600x900.\n   */\n  resizeTo() {\n    const screen = this.rendererManager.getRenderer()?.screen;\n    // Should not be possible\n    if (!screen) return;\n    // Using caching to check if unchanged\n    if (this.screenWidth === screen.width && this.screenHeight === screen.height) return;\n    [this.screenWidth, this.screenHeight] = [screen.width, screen.height];\n\n    let widthInPx, heightInPx;\n    // Determines whether we will have black stripes vertically or horizontally\n    if (screen.width < screen.height * ratio) {\n      widthInPx = screen.width;\n      heightInPx = screen.width / ratio;\n    } else {\n      // That is usually the case that the height is the constraint\n      widthInPx = screen.height * ratio;\n      heightInPx = screen.height;\n    }\n\n    const scale = widthInPx / STAGE_WIDTH;\n    this.stage.scale.set(scale, scale);\n    this.stage.position.set((screen.width - widthInPx) / 2, (screen.height - heightInPx) / 2);\n    console.debug(`Resizing screen.dimensions = ${screen.width} x ${screen.height}, scale = ${scale}`);\n  }\n\n  updateAnalysisStage() {\n    // Maybe resizing should also be push based (with some debouncing)\n    this.resizeTo();\n    this.playfield.updatePlayfield();\n    this.foregroundHUDPreparer.updateHUD();\n    this.keyPressOverlayPreparer.update();\n  }\n\n  destroy(): void {\n    // Do nothing\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/renderers/constants.ts",
    "content": "// The whole game stage will be in the virtual size 1600x900 (16:9) format\nexport const STAGE_WIDTH = 1600;\nexport const STAGE_HEIGHT = 900;\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/textures/TextureManager.ts",
    "content": "import { injectable } from \"inversify\";\nimport { Texture } from \"pixi.js\";\n\nconst RewindTextures = [\"BACKGROUND\"] as const;\n\nexport type RewindTextureId = typeof RewindTextures[number];\n\n@injectable()\nexport class TextureManager {\n  dict: Map<RewindTextureId, Texture> = new Map<RewindTextureId, Texture>();\n\n  /**\n   * Return with an empty fallback\n   * @param textureId\n   * @returns Texture.EMPTY in case it was not found\n   */\n  getTexture(textureId: RewindTextureId): Texture {\n    const texture = this.dict.get(textureId);\n    return texture ?? Texture.EMPTY;\n  }\n\n  async loadTexture(url: string) {\n    try {\n      return await Texture.fromURL(url);\n    } catch (err) {\n      return Texture.EMPTY;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/services/types/index.ts",
    "content": "export const STAGE_TYPES = {\n  AUDIO_CONTEXT: Symbol.for(\"AUDIO_CONTEXT\"),\n  EVENT_EMITTER: Symbol.for(\"EVENT_EMITTER\"),\n  ELECTRON_STORE: Symbol.for(\"ELECTRON_STORE\"),\n  REWIND_SKINS_FOLDER: Symbol.for(\"REWIND_SKINS_FOLDER\"),\n  APP_VERSION: Symbol.for(\"APP_VERSION\"),\n  APP_PLATFORM: Symbol.for(\"APP_PLATFORM\"),\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/store/index.tsx",
    "content": "import { configureStore } from \"@reduxjs/toolkit\";\nimport settingsReducer from \"./settings/slice\";\nimport updaterReducer from \"./update/slice\";\nimport { setupListeners } from \"@reduxjs/toolkit/query\";\n\n// TODO: Maybe remove and just use RXJS only\n\nconst reducer = {\n  settings: settingsReducer,\n  updater: updaterReducer,\n};\n\nconst isDev = process.env.NODE_ENV !== \"production\" && false;\nconst preloadedState = {\n  router: {},\n};\n\nconst store = configureStore({\n  devTools: process.env.NODE_ENV !== \"production\",\n  reducer,\n});\n\n// Setting up redux-toolkit API listeners\nsetupListeners(store.dispatch);\n\nexport type RootState = ReturnType<typeof store.getState>;\nexport type AppDispatch = typeof store.dispatch;\n\nexport { store };\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/store/settings/slice.ts",
    "content": "import { createSlice } from \"@reduxjs/toolkit\";\n\ninterface RewindSettingsState {\n  open: boolean;\n}\n\nconst initialState: RewindSettingsState = {\n  open: false,\n};\n\nconst rewindSettingsSlice = createSlice({\n  name: \"settings\",\n  initialState,\n  reducers: {\n    settingsModalClosed(state) {\n      state.open = false;\n    },\n    settingsModalOpened(state) {\n      state.open = true;\n    },\n  },\n});\n\nexport const { settingsModalClosed, settingsModalOpened } = rewindSettingsSlice.actions;\nexport default rewindSettingsSlice.reducer;\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/store/update/slice.ts",
    "content": "import { createSlice, PayloadAction } from \"@reduxjs/toolkit\";\n\ninterface UpdateStatus {\n  newVersion: string | null;\n  modalOpen: boolean;\n\n  //\n  downloadedBytes: number;\n  totalBytes: number;\n  bytesPerSecond: number;\n\n  isDownloading: boolean;\n  downloadFinished: boolean;\n  error: boolean;\n}\n\nconst initialState: UpdateStatus = {\n  // TODO: This is for testing\n  // modalOpen: true,\n  // newVersion: \"3.4.1\",\n  // bytesPerSecond: 20,\n  // totalBytes: 3333,\n  // downloadedBytes: 3333,\n  //\n  // downloadFinished: true,\n  // isDownloading: true,\n  // error: false,\n\n  newVersion: null,\n  modalOpen: false,\n  bytesPerSecond: 0,\n  totalBytes: 0,\n  downloadedBytes: 0,\n\n  downloadFinished: false,\n  isDownloading: false,\n  error: false,\n};\n\nconst updateSlice = createSlice({\n  name: \"update\",\n  initialState,\n  reducers: {\n    newVersionAvailable(draft, action: PayloadAction<string>) {\n      draft.newVersion = action.payload;\n      draft.modalOpen = true;\n    },\n    setUpdateModalOpen(draft, action: PayloadAction<boolean>) {\n      draft.modalOpen = action.payload;\n    },\n    downloadProgressed(\n      draft,\n      action: PayloadAction<{ totalBytes: number; bytesPerSecond: number; downloadedBytes: number }>,\n    ) {\n      const { downloadedBytes, totalBytes, bytesPerSecond } = action.payload;\n      draft.isDownloading = true;\n      draft.bytesPerSecond = bytesPerSecond;\n      draft.totalBytes = totalBytes;\n      draft.downloadedBytes = downloadedBytes;\n    },\n    downloadFinished(draft) {\n      draft.downloadFinished = true;\n    },\n    downloadErrorHappened(draft) {\n      draft.error = true;\n    },\n  },\n});\n\nexport const { newVersionAvailable, setUpdateModalOpen, downloadFinished, downloadProgressed } = updateSlice.actions;\n\nexport default updateSlice.reducer;\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/styles/theme.ts",
    "content": "import { createTheme } from \"@mui/material\";\n\nexport const rewindTheme = createTheme({\n  palette: {\n    mode: \"dark\",\n    background: {},\n  },\n  components: {\n    MuiButtonBase: {\n      defaultProps: {\n        disableRipple: true,\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/utils/constants.ts",
    "content": "const osuUniDiscord = \"https://discord.gg/QubdHdnBVg\";\nexport const discordUrl = osuUniDiscord;\nexport const twitterUrl = \"https://twitter.com/osuuniversity\";\nexport const youtubeUrl = \"https://www.youtube.com/channel/UCzW2Z--fEw0LWKgVTmO-b6w\";\nexport const RewindLinks = {\n  OsuPpyShAbstrakt: \"https://osu.ppy.sh/users/5773957\",\n  OsuUniDiscord: osuUniDiscord,\n  Guide: \"https://bit.ly/3BOF3P2\",\n};\nexport const ALLOWED_SPEEDS = [0.25, 0.75, 1.0, 1.5, 2.0, 4.0];\n// Currently, it's a feature that is hard to implement, enable it once ready\nexport const ELECTRON_UPDATE_FLAG = false;\nexport const PlaybarColors = {\n  MISS: \"rgb(255,0,0)\",\n  SLIDER_BREAK: \"rgb(255,89,0)\",\n  MEH: \"rgb(255,221,0)\",\n  OK: \"rgb(102,255,0)\",\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/utils/focus.ts",
    "content": "import React from \"react\";\n\nexport function ignoreFocus(event: React.FocusEvent<HTMLButtonElement>) {\n  event.target.blur();\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/utils/pooling/ObjectPool.ts",
    "content": "type Creator<T> = () => T;\ntype CleanUp<T> = (t: T) => unknown;\n\ninterface Options {\n  initialSize: number;\n}\n\nconst defaultOptions: Options = {\n  initialSize: 10,\n};\n\nexport class ObjectPool<T> {\n  protected pool: T[];\n  protected dict: Map<string, T>;\n\n  constructor(\n    private readonly creator: Creator<T>,\n    private readonly cleanup: CleanUp<T>,\n    options: Options = defaultOptions,\n  ) {\n    this.pool = [];\n    for (let i = 0; i < options.initialSize; i++) {\n      this.pool.push(creator());\n    }\n    this.dict = new Map<string, T>();\n  }\n\n  // The boolean tells you if it was taken from cache\n  allocate(id: string): [T, boolean] {\n    const cached = this.dict.get(id);\n    if (cached !== undefined) {\n      return [cached, true];\n    }\n    let lastElement = this.pool.pop();\n    if (!lastElement) {\n      lastElement = this.creator();\n    }\n    this.dict.set(id, lastElement);\n    return [lastElement, false];\n  }\n\n  release(id: string) {\n    const value = this.dict.get(id);\n    if (value === undefined) return;\n    this.cleanup(value);\n    this.pool.push(value);\n    this.dict.delete(id);\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/utils/pooling/TemporaryObjectPool.ts",
    "content": "// For each round it will record those that have been accessed and after the .end() it will release all that have\n\nimport { ObjectPool } from \"./ObjectPool\";\n\n// Releases those that have not been touched.\nexport class TemporaryObjectPool<T> extends ObjectPool<T> {\n  touched: Set<string> = new Set<string>();\n\n  allocate(id: string) {\n    this.touched.add(id);\n    return super.allocate(id);\n  }\n\n  releaseUntouched() {\n    // Need to copy it because also delete from it at the same time\n    const keys = Array.from(this.dict.keys());\n    for (const id of keys) {\n      if (!this.touched.has(id)) {\n        this.release(id);\n      }\n    }\n    this.touched.clear();\n  }\n}\n"
  },
  {
    "path": "apps/desktop/frontend/src/app/utils/replay.ts",
    "content": "import { OsuAction, ReplayFrame } from \"@osujs/core\";\nimport { Position, Vec2 } from \"@osujs/math\";\n\n// TODO: Could be useful in osu/core\n// Finds the index of the highest time that is not greater the given one.\n// Returns -1 if not found\nexport function findIndexInReplayAtTime(replay: ReplayFrame[], time: number): number {\n  let lo = 0;\n  let hi = replay.length;\n  while (lo < hi) {\n    const mid = (lo + hi) >> 1;\n    if (replay[mid].time <= time) {\n      lo = mid + 1;\n    } else {\n      hi = mid;\n    }\n  }\n  // return replay.findIndex((frame) => time < frame.time);\n  return lo - 1;\n}\n\n/**\n * Interpolates the position between two frames (since technically speaking there is no information between those times)\n */\nexport function interpolateReplayPosition(fA: ReplayFrame, fB: ReplayFrame, time: number): Position {\n  if (fB.time === fA.time) {\n    return fA.position;\n  } else {\n    const p = (time - fA.time) / (fB.time - fA.time);\n    return Vec2.interpolate(fA.position, fB.position, p);\n  }\n}\n\n// ? needed ?\nexport const getPressesBooleanArray = (actions: OsuAction[]): [boolean, boolean] => {\n  return [OsuAction.leftButton, OsuAction.rightButton].map((b) => actions.includes(b)) as [boolean, boolean];\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/assets/.gitkeep",
    "content": ""
  },
  {
    "path": "apps/desktop/frontend/src/constants.ts",
    "content": ""
  },
  {
    "path": "apps/desktop/frontend/src/environments/environment.prod.ts",
    "content": "export const environment = {\n  production: true,\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/environments/environment.ts",
    "content": "// This file can be replaced during build by using the `fileReplacements` array.\n// When building for production, this file is replaced with `environment.prod.ts`.\n\nexport const environment = {\n  production: false,\n  debugAnalyzer: {\n    replayPath:\n      \"D:/osu!/Replays/abstrakt - Bliitzit - Team Magma & Aqua Leader Battle Theme (Unofficial) [SMOKELIND's Insane] (2022-03-15) Osu.osr\",\n  },\n};\n"
  },
  {
    "path": "apps/desktop/frontend/src/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>Rewind</title>\n    <base href=\"/\" />\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"icon\" type=\"image/x-icon\" href=\"favicon.ico\" />\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/desktop/frontend/src/main.tsx",
    "content": "import { StrictMode } from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nimport { Provider } from \"react-redux\";\nimport { store } from \"./app/store\";\n\nimport { RewindApp } from \"./app/RewindApp\";\nimport { CssBaseline, ThemeProvider } from \"@mui/material\";\nimport { HashRouter } from \"react-router-dom\";\nimport { rewindTheme } from \"./app/styles/theme\";\nimport { TheaterProvider } from \"./app/providers/TheaterProvider\";\nimport { createRewindTheater } from \"./app/services/common/CommonManagers\";\nimport { frontendAPI } from \"./app/api\";\nimport log from \"electron-log\";\nimport { join } from \"path\";\n\n(async function() {\n  // Recommended way to use electron-log whenever we write console.log\n  Object.assign(console, log.functions);\n\n  const [appVersion, appPlatform, appResourcesPath] = await Promise.all([\n    frontendAPI.getAppVersion(),\n    frontendAPI.getPlatform(),\n    frontendAPI.getPath(\"appResources\"),\n  ]);\n  const rewindSkinsFolder = join(appResourcesPath, \"Skins\");\n\n  console.log(`Booting Rewind application with ${JSON.stringify({ appVersion, appPlatform, rewindSkinsFolder })}`);\n\n  const theater = createRewindTheater({\n    appVersion,\n    appPlatform,\n    rewindSkinsFolder,\n  });\n\n  const container = document.getElementById(\"root\") as HTMLElement;\n  const root = createRoot(container);\n  root.render(\n    <StrictMode>\n      <Provider store={store}>\n        {/* Using `HashRouter` due to Electron https://github.com/remix-run/history/blob/dev/docs/api-reference.md#createhashhistory */}\n        <HashRouter>\n          <ThemeProvider theme={rewindTheme}>\n            <CssBaseline />\n            <TheaterProvider theater={theater}>\n              <RewindApp />\n            </TheaterProvider>\n          </ThemeProvider>\n        </HashRouter>\n      </Provider>\n    </StrictMode>,\n  );\n})();\n"
  },
  {
    "path": "apps/desktop/frontend/src/polyfills.ts",
    "content": "/**\n * Polyfill stable language features. These imports will be optimized by `@babel/preset-env`.\n *\n * See: https://github.com/zloirock/core-js#babel\n */\nimport \"core-js/stable\";\nimport \"regenerator-runtime/runtime\";\nimport \"reflect-metadata\";\n"
  },
  {
    "path": "apps/desktop/frontend/src/styles.css",
    "content": "/* You can add global styles to this file, and also import other style files */\n"
  },
  {
    "path": "apps/desktop/frontend/test/ajv.spec.ts",
    "content": "import Ajv from \"ajv\";\nimport { SkinSettings, SkinSettingsSchema } from \"../src/app/services/common/skin\";\n\ndescribe(\"validateSkinSettings\", () => {\n  const ajv = new Ajv({ useDefaults: true });\n  const validateSkinSettings = ajv.compile<SkinSettings>(SkinSettingsSchema);\n\n  it(\"normal behavior\", () => {\n    const data = {\n      preferredSkinId: \"#-------#--------------#---------- wow so many dashes v2 final final - copy\",\n    };\n    expect(validateSkinSettings(data)).toBe(true);\n  });\n  it(\"default behavior when missing\", () => {\n    const data = {};\n    expect(validateSkinSettings(data)).toBe(true);\n    expect(data).toEqual({\n      preferredSkinId: \"rewind:RewindDefaultSkin\",\n    });\n  });\n  it(\"default behavior when wrong data type\", () => {\n    const data = {\n      preferredSkinId: 42,\n    };\n    expect(validateSkinSettings(data)).toBe(false);\n    // This is where we just straight up pass in the default data.\n  });\n  it(\"type safe\", () => {\n    const data = {\n      preferredSkinId: \"x\",\n    };\n    if (validateSkinSettings(data)) {\n      // Data is now SkinSettings because of type predicate\n      expect(data.preferredSkinId).toEqual(\"x\");\n\n      // Below should not compile\n      // expect(data.preferredSkinId2).toEqual(\"x\");\n    }\n  });\n});\n"
  },
  {
    "path": "apps/desktop/frontend/tsconfig.app.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"types\": [\n      \"node\"\n    ]\n  },\n  \"files\": [\n    \"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts\",\n    \"../../../node_modules/@nrwl/react/typings/image.d.ts\"\n  ],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.js\",\n    \"**/*.jsx\",\n    \"**/*.ts\",\n    \"**/*.tsx\"\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/frontend/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"jsx\": \"react-jsx\",\n    \"allowJs\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"strict\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/frontend/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ],\n  \"files\": [\n    \"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts\",\n    \"../../node_modules/@nrwl/react/typings/image.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/frontend/webpack.config.js",
    "content": "const { merge } = require(\"webpack-merge\");\n\n// A bit of a hack since nx/web executors use target: 'web'\n//https://github.com/nrwl/nx/blob/master/packages/web/src/utils/config.ts\nmodule.exports = (config, context) => {\n  return merge(config, {\n    // overwrite values here\n    target: \"electron-renderer\",\n    externals: {\n      // https://github.com/yan-foto/electron-reload/issues/71#issuecomment-588988382\n      fsevents: \"require('fsevents')\"\n    }\n  });\n};\n"
  },
  {
    "path": "apps/desktop/main/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/main/README.md",
    "content": "In order to build the whole application we need to first build the projects respectively and then include\nthem while packaging with `electron-builder`.\n\nTODO: Remove package.json/yarn.lock or move them somewhere else\n"
  },
  {
    "path": "apps/desktop/main/electron-builder.json",
    "content": "{\n  \"appId\": \"sh.abstrakt.rewind\",\n  \"productName\": \"Rewind\",\n  \"nsis\": {\n    \"oneClick\": false,\n    \"allowToChangeInstallationDirectory\": true\n  },\n  \"generateUpdatesFilesForAllChannels\": true,\n  \"directories\": {\n    \"output\": \"dist/electron\",\n    \"buildResources\": \"../../tools/electron-builder/build-resources\"\n  },\n  \"files\": [\n    {\n      \"from\": \"../../dist/apps/rewind-electron\",\n      \"to\": \"rewind-electron\",\n      \"filter\": [\n        \"main.js\"\n      ]\n    },\n    {\n      \"from\": \"../../dist/apps/desktop-frontend\",\n      \"to\": \"desktop-frontend\"\n    },\n    {\n      \"from\": \"../../dist/apps/desktop-backend\",\n      \"to\": \"desktop-backend\"\n    },\n    {\n      \"from\": \"../../dist/apps/desktop-backend-preload\",\n      \"to\": \"desktop-backend-preload\"\n    },\n    {\n      \"from\": \"../../dist/apps/desktop-frontend-preload\",\n      \"to\": \"desktop-frontend-preload\"\n    },\n    \"index.js\",\n    \"package.json\"\n  ],\n  \"extraResources\": [\n    {\n      \"from\": \"../../resources/Skins\",\n      \"to\": \"Skins\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/main/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"main\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]s$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"js\", \"html\"],\n  coverageDirectory: \"../../../coverage/apps/main\",\n};\n"
  },
  {
    "path": "apps/desktop/main/package.json",
    "content": "{\n  \"private\": true,\n  \"name\": \"rewind\",\n  \"author\": \"Rewind\",\n  \"description\": \"Will this popup in installation?\",\n  \"version\": \"0.1.3-alpha.1\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"build\": \"electron-builder\",\n    \"postinstall\": \"electron-builder install-app-deps\"\n  },\n  \"devDependencies\": {\n    \"electron\": \"^16.0.4\",\n    \"electron-builder\": \"^22.14.5\"\n  },\n  \"dependencies\": {\n    \"@babel/core\": \"^7.16.0\",\n    \"@babel/preset-env\": \"^7.16.4\",\n    \"@babel/preset-react\": \"^7.16.0\",\n    \"@nestjs/common\": \"^8.2.3\",\n    \"@nestjs/core\": \"^8.2.3\",\n    \"@nestjs/event-emitter\": \"^1.0.0\",\n    \"@nestjs/platform-express\": \"^8.2.3\",\n    \"@nestjs/platform-socket.io\": \"^8.2.3\",\n    \"@nestjs/testing\": \"^8.2.3\",\n    \"@nestjs/websockets\": \"^8.2.3\",\n    \"ajv\": \"^8.8.2\",\n    \"chokidar\": \"^3.5.2\",\n    \"dotenv\": \"^10.0.0\",\n    \"electron-log\": \"^4.4.1\",\n    \"electron-updater\": \"^4.6.1\",\n    \"immer\": \"^9.0.7\",\n    \"nest-winston\": \"^1.6.2\",\n    \"node-osr\": \"^1.2.1\",\n    \"ojsama\": \"^2.2.0\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"rxjs\": \"^7.4.0\",\n    \"simple-statistics\": \"^7.7.0\",\n    \"socket.io\": \"^4.4.0\",\n    \"supertest\": \"^6.1.6\",\n    \"tslib\": \"^2.3.1\",\n    \"username\": \"^5.1.0\",\n    \"utility-types\": \"^3.10.0\",\n    \"winston\": \"^3.3.3\"\n  }\n}\n"
  },
  {
    "path": "apps/desktop/main/src/app/.gitkeep",
    "content": ""
  },
  {
    "path": "apps/desktop/main/src/app/config.ts",
    "content": "import Ajv, { JSONSchemaType } from \"ajv\";\nimport { join } from \"path\";\nimport { readFileSync } from \"fs\";\n\nconst ajv = new Ajv({ useDefaults: true });\nconst REWIND_CFG_NAME = \"rewind.cfg\";\n\nexport interface RewindElectronSettings {\n  osuPath: string;\n}\n\nconst DEFAULT_REWIND_ELECTRON_SETTINGS: RewindElectronSettings = Object.freeze({ osuPath: \"\" });\n\nexport const RewindElectronSettingsSchema: JSONSchemaType<RewindElectronSettings> = {\n  type: \"object\",\n  properties: {\n    osuPath: { type: \"string\", default: DEFAULT_REWIND_ELECTRON_SETTINGS.osuPath },\n  },\n  required: [],\n};\n\nconst validate = ajv.compile<RewindElectronSettings>(RewindElectronSettingsSchema);\n\nexport function readRewindElectronSettings(configPath: string) {\n  const file = join(configPath, REWIND_CFG_NAME);\n  let json;\n  try {\n    const str = readFileSync(file, \"utf-8\");\n    json = JSON.parse(str);\n  } catch (e) {\n    console.warn(\"Could not parse the config correctly:\\n\" + e);\n    return DEFAULT_REWIND_ELECTRON_SETTINGS;\n  }\n  if (!validate(json)) {\n    console.warn(\n      \"Could not validate the config JSON correctly. Do not modify the file if you are not sure what you are doing.\",\n    );\n    return DEFAULT_REWIND_ELECTRON_SETTINGS;\n  }\n  return json;\n}\n"
  },
  {
    "path": "apps/desktop/main/src/app/events.ts",
    "content": "import { app, dialog, ipcMain } from \"electron\";\nimport { read } from \"node-osr\";\n\nasync function userSelectDirectory(defaultPath: string) {\n  const { canceled, filePaths } = await dialog.showOpenDialog({ defaultPath, properties: [\"openDirectory\"] });\n  if (canceled || filePaths.length === 0) {\n    return null;\n  } else {\n    return filePaths[0];\n  }\n}\n\nasync function userSelectFile(defaultPath: string) {\n  const { canceled, filePaths } = await dialog.showOpenDialog({ defaultPath, properties: [\"openFile\"] });\n  if (canceled || filePaths.length === 0) {\n    return null;\n  } else {\n    return filePaths[0];\n  }\n}\n\nfunction getPath(type: string) {\n  switch (type) {\n    case \"appData\":\n      return app.getPath(\"appData\");\n    case \"userData\":\n      return app.getPath(\"userData\");\n    case \"logs\":\n      return app.getPath(\"logs\");\n    case \"appResources\":\n      return process.resourcesPath;\n  }\n  return \"\";\n}\n\nexport function setupEventListeners() {\n  ipcMain.handle(\"getPath\", (event, type) => getPath(type));\n  ipcMain.handle(\"selectDirectory\", (event, defaultPath) => userSelectDirectory(defaultPath));\n  ipcMain.handle(\"selectFile\", (event, defaultPath) => userSelectFile(defaultPath));\n  ipcMain.handle(\"getPlatform\", () => process.platform);\n  ipcMain.handle(\"getAppVersion\", () => app.getVersion());\n  ipcMain.handle(\"reboot\", () => {\n    app.relaunch();\n    app.quit();\n  });\n\n  ipcMain.handle(\"readOsr\", async (event, filePath) => {\n    return await read(filePath);\n  });\n}\n"
  },
  {
    "path": "apps/desktop/main/src/app/updater.ts",
    "content": "import { prerelease } from \"semver\";\nimport { app, ipcMain } from \"electron\";\nimport { autoUpdater, ProgressInfo, UpdateInfo } from \"electron-updater\";\nimport log from \"electron-log\";\nimport { windows } from \"./windows\";\n\n/**\n * Basically once the user has signed to opt-in to the alpha version, it will always get the alpha versions.\n * @param version\n */\nexport function channelToUse(version: string) {\n  const pre = prerelease(version);\n  if (pre !== null) return { channel: \"alpha\", allowPrerelease: true };\n  return { channel: \"latest\", allowPrerelease: false };\n}\n\nconst oneMinute = 60 * 1000;\nconst fifteenMinutes = 15 * oneMinute;\nconst units = [\"bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"];\n\nfunction niceBytes(x: any) {\n  let l = 0,\n    n = parseInt(x, 10) || 0;\n  while (n >= 1024 && ++l) {\n    n = n / 1024;\n  }\n  return n.toFixed(n < 10 && l > 0 ? 1 : 0) + \" \" + units[l];\n}\n\nfunction checkForUpdates() {\n  void autoUpdater.checkForUpdates().then((value) => {\n    log.info(\"[checkForUpdates] \", JSON.stringify(value));\n  });\n}\n\nfunction pollForUpdates() {\n  // The first update should be manually done by the frontend since it might have not set up the listeners\n  // checkForUpdates();\n  setInterval(() => {\n    checkForUpdates();\n  }, fifteenMinutes);\n}\n\nfunction attachListeners() {\n  // https://www.electron.build/auto-update#event-update-downloaded\n  autoUpdater.on(\"download-progress\", (info: ProgressInfo) => {\n    log.info(\"[Updater] Update download progress \", info);\n    const { delta, percent, total, transferred, bytesPerSecond } = info;\n    windows.frontend?.webContents.send(\"onUpdateDownloadProgress\", { total, transferred, bytesPerSecond });\n  });\n\n  autoUpdater.on(\"error\", (error) => {\n    console.log(error);\n  });\n\n  autoUpdater.on(\"update-available\", (info: UpdateInfo) => {\n    log.info(\"[Updater] Update available: \", info);\n    windows.frontend?.webContents.send(\"onUpdateAvailable\", info.version);\n  });\n\n  autoUpdater.on(\"update-downloaded\", async (info: UpdateInfo) => {\n    log.info(\"[Updater] The new update was downloaded.\");\n    windows.frontend?.webContents.send(\"onDownloadFinished\");\n  });\n\n  // In case a manual update check was done, which is not supported yet.\n  autoUpdater.on(\"update-not-available\", () => {\n    log.info(\"[Updater] No new update has been found.\");\n  });\n\n  ipcMain.handle(\"startDownloadingUpdate\", () => {\n    // Cancellation token can be given\n    void autoUpdater.downloadUpdate();\n  });\n  ipcMain.handle(\"checkForUpdate\", () => {\n    checkForUpdates();\n  });\n  ipcMain.handle(\"quitAndInstall\", () => {\n    void autoUpdater.quitAndInstall(true, true);\n  });\n}\n\nexport function initializeAutoUpdater() {\n  // Checking for updates for Windows NSIS installations only\n  const { channel, allowPrerelease } = channelToUse(app.getVersion());\n  autoUpdater.channel = channel;\n  autoUpdater.allowPrerelease = allowPrerelease;\n  autoUpdater.logger = log;\n  // Needed\n  log.transports.file.level = \"info\";\n\n  autoUpdater.autoDownload = false;\n  log.info(`Initialized auto-updater with allowPrerelease=${allowPrerelease} and channel=${channel}`);\n  app.whenReady().then(async () => {\n    log.info(\"WhenReady called\");\n    attachListeners();\n    pollForUpdates();\n  });\n}\n"
  },
  {
    "path": "apps/desktop/main/src/app/windows.ts",
    "content": "import { BrowserWindow } from \"electron\";\n\nexport interface Windows {\n  frontend: null | BrowserWindow;\n}\n\nexport const windows: Windows = {\n  frontend: null,\n};\n"
  },
  {
    "path": "apps/desktop/main/src/assets/.gitkeep",
    "content": ""
  },
  {
    "path": "apps/desktop/main/src/environments/environment.prod.ts",
    "content": "export const environment = {\n  production: true,\n};\n"
  },
  {
    "path": "apps/desktop/main/src/environments/environment.ts",
    "content": "export const environment = {\n  production: false,\n};\n"
  },
  {
    "path": "apps/desktop/main/src/index.ts",
    "content": "import { app, BrowserWindow, dialog, ipcMain, Menu, screen, shell } from \"electron\";\nimport { setupEventListeners } from \"./app/events\";\nimport { windows } from \"./app/windows\";\nimport { join } from \"path\";\nimport { format } from \"url\";\nimport { environment } from \"./environments/environment\";\nimport log from \"electron-log\";\n\nconst { ELECTRON_IS_DEV } = process.env;\n\n// TODO: Refactor ???\nfunction isDevelopmentMode() {\n  if (ELECTRON_IS_DEV) return true;\n  return !environment.production;\n}\n\nconst DEFAULT_WIDTH = 1600;\nconst DEFAULT_HEIGHT = 900;\nconst MIN_WIDTH = 1024;\nconst MIN_HEIGHT = 600;\n\nconst rendererAppDevPort = 4200;\n\n/**\n * The folder tree inside the Electron application will approximately look like as follows:\n *\n * ./index.js [THIS FILE]\n * ./frontend/(index.html|main.js|...)\n */\nconst desktopFrontendFile = (fileName: string) => join(__dirname, \"frontend\", fileName);\n\nfunction createFrontendWindow() {\n  const workAreaSize = screen.getPrimaryDisplay().workAreaSize;\n  const width = Math.min(DEFAULT_WIDTH, workAreaSize.width || DEFAULT_WIDTH);\n  const height = Math.min(DEFAULT_HEIGHT, workAreaSize.height || DEFAULT_HEIGHT);\n  const minWidth = Math.min(MIN_WIDTH, workAreaSize.width || MIN_WIDTH);\n  const minHeight = Math.min(MIN_HEIGHT, workAreaSize.height || MIN_HEIGHT);\n\n  const isDev = isDevelopmentMode();\n  const frontend = new BrowserWindow({\n    width,\n    height,\n    minWidth,\n    minHeight,\n    show: false,\n    webPreferences: {\n      // `webSecurity` disabled while developing, otherwise we can't do hot reloading conveniently\n      // https://stackoverflow.com/questions/50272451/electron-js-images-from-local-file-system\n      webSecurity: !isDev,\n      // This MUST be true in order for PageVisibility API to work.\n      backgroundThrottling: true,\n      nodeIntegration: true,\n      contextIsolation: false,\n    },\n  });\n  frontend.center();\n  frontend.on(\"ready-to-show\", () => {\n    windows.frontend?.show();\n  });\n  frontend.on(\"closed\", () => {\n    windows.frontend = null;\n    app.quit();\n  });\n  // Open external links such as socials in the default browser.\n  frontend.webContents.setWindowOpenHandler((details) => {\n    void shell.openExternal(details.url);\n    return { action: \"deny\" };\n  });\n\n  ipcMain.on(\"osuFolderChanged\", (event, folder: string) => {\n    // TODO: frontend.setMenu()\n    Menu.setApplicationMenu(createMenu(folder));\n  });\n\n  // In DEV mode we want to utilize hot reloading, therefore we are going to connect to the development server.\n  // Therefore, `nx run frontend:serve` must be run first before this is executed.\n  if (isDev) {\n    void frontend.loadURL(`http://localhost:${rendererAppDevPort}`);\n  } else {\n    void frontend.loadURL(\n      format({\n        pathname: desktopFrontendFile(\"index.html\"),\n        protocol: \"file:\",\n        slashes: true,\n      }),\n    );\n  }\n\n  windows.frontend = frontend;\n}\n\nfunction handleAllWindowClosed() {\n  // On macOS it's common that the application stays open and can be reactivated.\n  if (process.platform !== \"darwin\") {\n    app.quit();\n  }\n}\n\nfunction handleReady() {\n  const isDev = isDevelopmentMode();\n\n  Menu.setApplicationMenu(createMenu(null));\n\n  console.log(\n    \"Booting Electron application with settings: \",\n    JSON.stringify({\n      isDev,\n      appDataPath: app.getPath(\"appData\"),\n    }),\n  );\n\n  createFrontendWindow();\n}\n\nfunction handleActivate() {\n  // On macOS it's common to re-create a window in the app when the\n  // dock icon is clicked and there are no other windows open.\n  if (windows.frontend === null) {\n    handleReady();\n  }\n}\n\n(function main() {\n  // Recommended way to use electron-log whenever we write console.log\n  Object.assign(console, log.functions);\n\n  // Make sure that it's only started once\n  const isLocked = app.requestSingleInstanceLock();\n  if (!isLocked) app.quit();\n\n  // Required for `electron.Notification` to work\n  app.setAppUserModelId(\"sh.abstrakt.rewind\");\n\n  // https://peter.sh/experiments/chromium-command-line-switches/\n  // So that the audio can't be stopped with media keys\n  app.commandLine.appendSwitch(\"disable-features\", \"HardwareMediaKeyHandling\");\n\n  // Even though some GPUs might be \"crashy\" (see\n  // https://chromium.googlesource.com/chromium/src/gpu/+/master/config/software_rendering_list.json), we still want to\n  // use it instead of rendering a power-point presentation.\n  app.commandLine.appendSwitch(\"ignore-gpu-blocklist\");\n\n  // TODO: Enable this once it's implemented properly\n  // initializeAutoUpdater();\n  setupEventListeners();\n\n  app.on(\"window-all-closed\", handleAllWindowClosed);\n  app.on(\"ready\", handleReady);\n  app.on(\"activate\", handleActivate);\n})();\n\nfunction createMenu(osuFolder: string | null) {\n  const osuFolderKnown = !!osuFolder as boolean;\n  const isMac = process.platform === \"darwin\";\n\n  const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [];\n  if (isMac) {\n    template.push({\n      label: app.name,\n      submenu: [\n        {\n          role: \"about\",\n          label: \"About\",\n        },\n        {\n          type: \"separator\",\n        },\n        {\n          role: \"services\",\n          label: \"Services\",\n        },\n        {\n          type: \"separator\",\n        },\n        {\n          role: \"hide\",\n          label: \"Hide\",\n        },\n        {\n          role: \"hideOthers\",\n          label: \"Hide Others\",\n        },\n        {\n          role: \"unhide\",\n          label: \"Unhide\",\n        },\n        {\n          type: \"separator\",\n        },\n        {\n          role: \"quit\",\n          label: \"Quit\",\n        },\n      ],\n    });\n  }\n\n  template.push(\n    {\n      label: \"File\",\n      submenu: [\n        {\n          label: \"Open Replay\",\n          accelerator: \"CommandOrControl+O\",\n          enabled: osuFolderKnown,\n          click: async () => {\n            const { canceled, filePaths } = await dialog.showOpenDialog({\n              defaultPath: join(osuFolder ?? \"\", \"Replays\"),\n              properties: [\"openFile\"],\n              filters: [\n                { name: \"osu! Replay\", extensions: [\"osr\"] },\n                { name: \"All Files\", extensions: [\"*\"] },\n              ],\n            });\n            if (!canceled && filePaths.length > 0) {\n              const file = filePaths[0];\n              windows.frontend?.webContents.send(\"onManualReplayOpen\", file);\n            }\n          },\n        },\n        { type: \"separator\" },\n        {\n          label: \"Open Installation Folder\",\n          click: async () => {\n            await shell.showItemInFolder(app.getPath(\"exe\"));\n          },\n        },\n        {\n          label: \"Open User Config Folder\",\n          click: async () => {\n            await shell.openPath(app.getPath(\"userData\"));\n          },\n        },\n        {\n          label: \"Open Logs Folder\",\n          click: async () => {\n            await shell.openPath(app.getPath(\"logs\"));\n          },\n        },\n        { type: \"separator\" },\n        {\n          label: \"Open osu! Folder\",\n          click: async () => {\n            if (osuFolder) await shell.openPath(osuFolder);\n          },\n          enabled: osuFolderKnown,\n        },\n        { type: \"separator\" },\n        { role: \"close\" },\n      ],\n    },\n    {\n      label: \"View\",\n      submenu: [{ role: \"reload\" }, { role: \"forceReload\" }, { type: \"separator\" }, { role: \"toggleDevTools\" }],\n    },\n    {\n      label: \"Help\",\n      submenu: [\n        {\n          label: \"Documentation\",\n          click: async () => {\n            await shell.openExternal(\"https://rewind.abstrakt.sh/docs/intro\");\n          },\n        },\n        {\n          label: \"Discord\",\n          click: async () => {\n            await shell.openExternal(\"https://discord.gg/QubdHdnBVg\");\n          },\n        },\n        { type: \"separator\" },\n        {\n          label: \"About\",\n          click: async () => {\n            const aboutMessage = `Rewind ${app.getVersion()}\\nDeveloped by abstrakt`;\n            await dialog.showMessageBox({\n              title: \"About Rewind\",\n              type: \"info\",\n              message: aboutMessage,\n              // TODO: icon: NativeImage (RewindIcon)\n            });\n          },\n        },\n      ],\n    },\n  );\n\n  return Menu.buildFromTemplate(template);\n}\n"
  },
  {
    "path": "apps/desktop/main/tsconfig.app.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"strict\": true,\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"node\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/main/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/desktop/main/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\"jest\", \"node\"]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "apps/web/backend/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/backend/README.md",
    "content": "Legacy project that was used as a renderer in the Electron app. Might be deleted or used later on.\n"
  },
  {
    "path": "apps/web/backend/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"web-backend\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]s$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"js\", \"html\"],\n  coverageDirectory: \"../../../coverage/apps/web-backend\",\n};\n"
  },
  {
    "path": "apps/web/backend/src/DesktopAPI.ts",
    "content": "import { ModuleRef, NestFactory } from \"@nestjs/core\";\nimport { NestExpressApplication } from \"@nestjs/platform-express\";\nimport { join } from \"path\";\nimport { Logger, Module, OnModuleInit } from \"@nestjs/common\";\nimport { osuFolderSanityCheck } from \"./config/utils\";\nimport { DesktopConfigService, REWIND_CFG_PATH } from \"./config/DesktopConfigService\";\nimport { DesktopConfigController } from \"./config/DesktopConfigController\";\nimport { NormalStatusController, SetupStatusController } from \"./status/SetupStatusController\";\nimport { EventEmitterModule } from \"@nestjs/event-emitter\";\nimport { WinstonModule } from \"nest-winston\";\nimport * as winston from \"winston\";\nimport { format } from \"winston\";\nimport username = require(\"username\");\nimport { determineSongsFolder } from \"@rewind/osu-local/utils\";\nimport { SkinNameResolver, SkinNameResolverConfig, SKIN_NAME_RESOLVER_CONFIG } from \"./skins/SkinNameResolver\";\nimport { LocalReplayController } from \"./replays/LocalReplayController\";\nimport { LocalBlueprintController } from \"./blueprints/LocalBlueprintController\";\nimport { SkinController } from \"./skins/SkinController\";\nimport { OSU_FOLDER, OSU_SONGS_FOLDER } from \"./constants\";\nimport { SkinService } from \"./skins/SkinService\";\nimport { LocalReplayService } from \"./replays/LocalReplayService\";\nimport { ReplayWatcher } from \"./replays/ReplayWatcher\";\nimport { LocalBlueprintService } from \"./blueprints/LocalBlueprintService\";\nimport { OsuDBDao } from \"./blueprints/OsuDBDao\";\nimport { EventsGateway } from \"./events/EventsGateway\";\n\nconst globalPrefix = \"/api\";\nconst REWIND_CFG_NAME = \"rewind.cfg\";\n\nexport interface RewindBootstrapSettings {\n  // Usually %APPDATA%\n  appDataPath: string;\n  // Usually %APPDATA%/rewind\n  userDataPath: string;\n  // Usually where `${rewind_installation_path}/resources`\n  appResourcesPath: string;\n\n  // Where to store the logs\n  logDirectory: string;\n}\n\ninterface NormalBootstrapSettings {\n  osuFolder: string;\n}\n\nconst port = process.env.PORT || 7271;\n\nfunction listenCallback() {\n  Logger.log(`Listening at http://localhost:${port}${globalPrefix}`);\n}\n\nfunction createLogger(logDirectory: string) {\n  const fileFormat = format.combine(\n    format.timestamp(),\n    format.align(),\n    format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`),\n  );\n  return WinstonModule.createLogger({\n    level: \"info\",\n    format: fileFormat,\n    transports: [\n      new winston.transports.File({ filename: join(logDirectory, \"error.log\"), level: \"error\" }),\n      new winston.transports.File({ filename: join(logDirectory, \"combined.log\") }),\n      new winston.transports.Console({\n        format: format.combine(format.colorize(), fileFormat),\n      }),\n    ],\n  });\n}\n\n/**\n * The usual bootstrap happens with the concrete knowledge of the osu! folder. Only at the first start up of the\n * application we will have to refer to boot differently.\n */\nasync function normalBootstrap(settings: {\n  osuFolder: string;\n  songsFolder: string;\n  userDataPath: string;\n  appResourcesPath: string;\n  logDirectory: string;\n}) {\n  const { osuFolder, userDataPath, appResourcesPath, logDirectory, songsFolder } = settings;\n  // Find out osu! folder through settings\n  const rewindCfgPath = getRewindCfgPath(userDataPath);\n  const skinNameResolverConfig: SkinNameResolverConfig = [\n    { prefix: \"osu\", path: join(osuFolder, \"Skins\") },\n    { prefix: \"rewind\", path: join(appResourcesPath, \"Skins\") },\n  ];\n\n  @Module({\n    imports: [EventEmitterModule.forRoot()],\n    controllers: [\n      LocalReplayController,\n      SkinController,\n      LocalBlueprintController,\n      NormalStatusController,\n      DesktopConfigController,\n    ],\n    providers: [\n      { provide: OSU_FOLDER, useValue: osuFolder },\n      { provide: OSU_SONGS_FOLDER, useValue: songsFolder },\n      { provide: REWIND_CFG_PATH, useValue: rewindCfgPath },\n      { provide: SKIN_NAME_RESOLVER_CONFIG, useValue: skinNameResolverConfig },\n      SkinNameResolver,\n      SkinService,\n      EventsGateway,\n      ReplayWatcher,\n      LocalReplayService,\n      LocalBlueprintService,\n      OsuDBDao,\n      DesktopConfigService,\n    ],\n  })\n  class RewindDesktopModule implements OnModuleInit {\n    constructor(private moduleRef: ModuleRef) {}\n\n    async onModuleInit(): Promise<void> {\n      const [osuFolder, replayWatcher, localBlueprintService] = await Promise.all([\n        this.moduleRef.resolve(OSU_FOLDER),\n        this.moduleRef.resolve(ReplayWatcher),\n        this.moduleRef.resolve(LocalBlueprintService),\n      ]);\n\n      const replaysFolder = join(osuFolder, \"Replays\");\n      replayWatcher.watchForReplays(replaysFolder);\n\n      localBlueprintService\n        .getAllBlueprints()\n        .then((blueprints) => Logger.log(`Loaded all ${Object.keys(blueprints).length} blueprints.`));\n      // TODO: Emit and then set the status to booted\n      Logger.log(`RewindDesktopModule onModuleInit finished with settings: ${JSON.stringify(settings)}`);\n    }\n  }\n\n  const app = await NestFactory.create<NestExpressApplication>(RewindDesktopModule, {\n    logger: createLogger(logDirectory),\n  });\n\n  app.setGlobalPrefix(globalPrefix);\n  app.enableCors();\n\n  // So that \"rewind\" skins are also accessible\n  skinNameResolverConfig.forEach((config) => {\n    app.useStaticAssets(config.path, { prefix: `/static/skins/${config.prefix}` });\n  });\n  app.useStaticAssets(songsFolder, { prefix: \"/static/songs\" });\n  // app.useLogger();\n\n  await app.listen(port, listenCallback);\n}\n\ninterface SetupBootstrapSettings {\n  userDataPath: string;\n  logDirectory: string;\n}\n\nfunction getRewindCfgPath(applicationDataPath: string) {\n  return join(applicationDataPath, REWIND_CFG_NAME);\n}\n\nexport async function setupBootstrap({ userDataPath, logDirectory }: SetupBootstrapSettings) {\n  const rewindCfgPath = getRewindCfgPath(userDataPath);\n  const rewindCfgProvider = {\n    provide: REWIND_CFG_PATH,\n    useValue: rewindCfgPath,\n  };\n\n  @Module({\n    providers: [rewindCfgProvider, DesktopConfigService],\n    controllers: [DesktopConfigController, SetupStatusController],\n  })\n  class SetupModule {}\n\n  const app = await NestFactory.create<NestExpressApplication>(SetupModule, { logger: createLogger(logDirectory) });\n  app.setGlobalPrefix(globalPrefix);\n  app.enableCors();\n\n  await app.listen(port, listenCallback);\n  return app;\n}\n\nasync function readOsuFolder(applicationDataPath: string): Promise<string | undefined> {\n  try {\n    const service = new DesktopConfigService(getRewindCfgPath(applicationDataPath));\n    const config = await service.loadConfig();\n    return config.osuPath;\n  } catch (err) {\n    return undefined;\n  }\n}\n\nexport async function bootstrapRewindDesktopBackend(settings: RewindBootstrapSettings) {\n  console.log(`Bootstrapping with settings: ${JSON.stringify(settings)}`);\n  const { appDataPath, userDataPath, appResourcesPath, logDirectory } = settings;\n  const osuFolder = await readOsuFolder(userDataPath);\n\n  if (osuFolder === undefined || !(await osuFolderSanityCheck(osuFolder))) {\n    return setupBootstrap({ userDataPath, logDirectory });\n  } else {\n    // This is the fallback songs folder in case username can't be determined or config file is corrupt\n    let songsFolder = join(osuFolder, \"Songs\");\n    const userId = await username();\n    if (userId !== undefined) {\n      const configuredSongsFolder = await determineSongsFolder(osuFolder, userId);\n      if (configuredSongsFolder !== undefined) {\n        songsFolder = configuredSongsFolder;\n      }\n    }\n    return normalBootstrap({ userDataPath, osuFolder, songsFolder, appResourcesPath, logDirectory });\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/api-common.module.ts",
    "content": "import { Module } from \"@nestjs/common\";\nimport { EventEmitterModule } from \"@nestjs/event-emitter\";\nimport { LocalReplayController } from \"./replays/LocalReplayController\";\nimport { SkinController } from \"./skins/SkinController\";\nimport { LocalBlueprintController } from \"./blueprints/LocalBlueprintController\";\nimport { SkinService } from \"./skins/SkinService\";\nimport { EventsGateway } from \"./events/EventsGateway\";\nimport { ReplayWatcher } from \"./replays/ReplayWatcher\";\nimport { LocalReplayService } from \"./replays/LocalReplayService\";\nimport { LocalBlueprintService } from \"./blueprints/LocalBlueprintService\";\nimport { OsuDBDao } from \"./blueprints/OsuDBDao\";\n\n// TODO: Delete, pretty unnecessary\n@Module({\n  imports: [EventEmitterModule.forRoot()],\n  controllers: [LocalReplayController, SkinController, LocalBlueprintController],\n  providers: [SkinService, EventsGateway, ReplayWatcher, LocalReplayService, LocalBlueprintService, OsuDBDao],\n})\nexport class ApiCommonModule {}\n"
  },
  {
    "path": "apps/web/backend/src/assets/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Rewind Desktop API</title>\n</head>\n<body>\n\nThis window (Renderer) runs the small \"backend\" for the Rewind application .\n\nOpen the console (Ctrl+Shift+I) to see the API logs.\n\n<!--\nhttps://github.com/electron/electron/issues/2863#issuecomment-142158983\nCan't use  <script src=\"...\">, we MUST use `require`.\n-->\n<script>\n  require(\"../main.js\");\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "apps/web/backend/src/blueprints/BlueprintInfo.ts",
    "content": "// TODO: Maybe a better name would be BlueprintMetadata ...\n// We DO parse the blue print (.osu), but that's only because we want to retrieve the metadata.\nexport interface BlueprintInfo {\n  md5Hash: string;\n  lastPlayed: Date;\n  title: string;\n  artist: string;\n  creator: string;\n  folderName: string;\n  audioFileName: string;\n  osuFileName: string;\n\n  // [Events]\n  bgFileName?: string; // Usually unknown unless .osu file is parsed\n}\n"
  },
  {
    "path": "apps/web/backend/src/blueprints/LocalBlueprintController.ts",
    "content": "import { Controller, Get, Param, Res } from \"@nestjs/common\";\nimport { LocalBlueprintService } from \"./LocalBlueprintService\";\nimport { Response } from \"express\";\n\n/**\n * The md5hash can be considered as the identifier the blueprint.\n */\n@Controller(\"blueprints\")\nexport class LocalBlueprintController {\n  constructor(private readonly blueprintService: LocalBlueprintService) {}\n\n  async blueprint(md5: string) {\n    const b = await this.blueprintService.getBlueprintByMD5(md5);\n    if (b === undefined) {\n      throw Error(`Blueprint with md5=${md5} not found`);\n    }\n    return b;\n  }\n\n  @Get(\":md5hash\")\n  async getBlueprintByMD5(@Res() res: Response, @Param(\"md5hash\") md5hash: string) {\n    const blueprintMetaData = await this.blueprintService.getBlueprintByMD5(md5hash);\n    res.json(blueprintMetaData);\n    // const path = this.songsFolder(blueprintMetaData.folderName, blueprintMetaData.osuFileName);\n  }\n\n  @Get(\":md5hash/osu\")\n  async getBlueprintOsu(@Res() res: Response, @Param(\"md5hash\") md5hash: string) {\n    const blueprintMetaData = await this.blueprint(md5hash);\n    return this.redirectToFolder(res, md5hash, blueprintMetaData.osuFileName);\n  }\n\n  @Get(\":md5hash/audio\")\n  async getBlueprintAudio(@Res() res: Response, @Param(\"md5hash\") md5hash: string) {\n    const { audioFileName } = await this.blueprint(md5hash);\n    return this.redirectToFolder(res, md5hash, audioFileName);\n  }\n\n  @Get(\":md5hash/bg\")\n  async getBlueprintBackground(@Res() res: Response, @Param(\"md5hash\") md5hash: string) {\n    const bgFileName = await this.blueprintService.blueprintBg(md5hash);\n    if (!bgFileName) throw Error(\"No background found\");\n    return this.redirectToFolder(res, md5hash, bgFileName);\n  }\n\n  @Get(\":md5hash/folder/:file\")\n  async redirectToFolder(@Res() res: Response, @Param(\"md5hash\") md5hash: string, @Param(\"file\") file: string) {\n    const blueprintMetaData = await this.blueprint(md5hash);\n    const { folderName } = blueprintMetaData;\n    // We need to encode the URI components for cases such as:\n    // \"E:\\osu!\\Songs\\1192060 Camellia - #1f1e33\\Camellia - #1f1e33 (Realazy) [Amethyst Storm].osu\"\n    // The two `#` characters need to be encoded to `%23` in both cases.\n    const url = `/static/songs/${encodeURIComponent(folderName)}/${encodeURIComponent(file)}`;\n    res.redirect(url);\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/blueprints/LocalBlueprintService.ts",
    "content": "import { Inject, Injectable, Logger } from \"@nestjs/common\";\nimport { BlueprintInfo } from \"./BlueprintInfo\";\nimport { OsuDBDao } from \"./OsuDBDao\";\nimport { promises as fsPromises } from \"fs\";\nimport { join } from \"path\";\nimport { filterFilenamesInDirectory } from \"@rewind/osu-local/utils\";\nimport { Blueprint, BlueprintSection, parseBlueprint } from \"@osujs/core\";\nimport { OSU_SONGS_FOLDER } from \"../constants\";\n\nimport { createHash } from \"crypto\";\n\nconst { stat, readFile } = fsPromises;\n\nconst METADATA_SECTIONS_TO_READ: BlueprintSection[] = [\"General\", \"Difficulty\", \"Events\", \"Metadata\"];\n\nfunction mapToLocalBlueprint(\n  blueprint: Blueprint,\n  osuFileName: string,\n  folderName: string,\n  md5Hash: string,\n): BlueprintInfo {\n  const { metadata } = blueprint.blueprintInfo;\n  return {\n    creator: \"\", // TODO: ?\n    title: metadata.title,\n    osuFileName,\n    folderName,\n    bgFileName: metadata.backgroundFile,\n    md5Hash,\n    audioFileName: metadata.audioFile,\n    artist: metadata.artist,\n    lastPlayed: new Date(),\n  };\n}\n\n@Injectable()\nexport class LocalBlueprintService {\n  blueprints: Record<string, BlueprintInfo> = {};\n  private logger = new Logger(LocalBlueprintService.name);\n\n  constructor(private readonly osuDbDao: OsuDBDao, @Inject(OSU_SONGS_FOLDER) private readonly songsFolder: string) {}\n\n  async completeRead() {\n    const freshBlueprints = await this.osuDbDao.getAllBlueprints();\n    const lastModifiedTime = await this.osuDbDao.getOsuDbLastModifiedTime();\n\n    this.logger.log(\"Reading the osu!/Songs folder\");\n    const candidates = await getNewFolderCandidates(this.songsFolder, new Date(lastModifiedTime));\n    if (candidates.length > 0) {\n      this.logger.log(`Candidates: ${candidates.length} : ${candidates.join(\",\")}`);\n    }\n    for (const songFolder of candidates) {\n      const osuFiles = await listOsuFiles(join(this.songsFolder, songFolder));\n      for (const osuFile of osuFiles) {\n        const fileName = join(this.songsFolder, songFolder, osuFile);\n        this.logger.log(`Reading file ${fileName}`);\n        const data = await readFile(fileName);\n        const hash = createHash(\"md5\");\n        hash.update(data);\n        const md5Hash = hash.digest(\"hex\");\n        const blueprint = await parseBlueprint(data.toString(\"utf-8\"), { sectionsToRead: METADATA_SECTIONS_TO_READ });\n        freshBlueprints.push(mapToLocalBlueprint(blueprint, osuFile, songFolder, md5Hash));\n      }\n    }\n\n    this.blueprints = {};\n    freshBlueprints.forEach(this.addNewBlueprint.bind(this));\n    return this.blueprints;\n  }\n\n  // Usually for watchers\n  addNewBlueprint(blueprint: BlueprintInfo) {\n    this.blueprints[blueprint.md5Hash] = blueprint;\n  }\n\n  async blueprintHasBeenAdded() {\n    // It's better to rely on the \"osu!.db\" than the ones we received from watching.\n    // But ofc, we if we read from osu!.db again there might still be some folders that have not been properly flushed.\n    if (await this.osuDbDao.hasChanged()) {\n      return true;\n    }\n    const s = await stat(this.songsFolder);\n    const t = await this.osuDbDao.getOsuDbLastModifiedTime();\n    return s.mtime.getTime() > t;\n  }\n\n  async getAllBlueprints(): Promise<Record<string, BlueprintInfo>> {\n    const needToCheckAgain = await this.blueprintHasBeenAdded();\n    if (needToCheckAgain) {\n      await this.completeRead();\n    }\n    return this.blueprints;\n  }\n\n  async getBlueprintByMD5(md5: string): Promise<BlueprintInfo | undefined> {\n    const maps = await this.getAllBlueprints();\n    return maps[md5];\n  }\n\n  // xd\n  async blueprintBg(md5: string): Promise<string | undefined> {\n    const blueprintMetaData = await this.getBlueprintByMD5(md5);\n    if (!blueprintMetaData) return undefined;\n    const { osuFileName, folderName } = blueprintMetaData;\n    const data = await readFile(join(this.songsFolder, folderName, osuFileName), { encoding: \"utf-8\" });\n    const parsedBlueprint = parseBlueprint(data, { sectionsToRead: [\"Events\"] });\n    // There is also an offset but let's ignore for now\n    return parsedBlueprint.blueprintInfo.metadata.backgroundFile;\n  }\n}\n\n// This might be a very expensive operation and should be done only once at startup. The new ones should be watched\n// ~250ms at my Songs folder\nexport async function getNewFolderCandidates(songsFolder: string, importedLaterThan: Date): Promise<string[]> {\n  return filterFilenamesInDirectory(songsFolder, async (fileName) => {\n    const s = await stat(join(songsFolder, fileName));\n    return s.mtime.getTime() > importedLaterThan.getTime() && s.isDirectory();\n  });\n}\n\nexport async function listOsuFiles(songFolder: string): Promise<string[]> {\n  return filterFilenamesInDirectory(songFolder, async (fileName) => fileName.endsWith(\".osu\"));\n}\n"
  },
  {
    "path": "apps/web/backend/src/blueprints/OsuDBDao.ts",
    "content": "import { promises } from \"fs\";\nimport { BlueprintInfo } from \"./BlueprintInfo\";\nimport { fileLastModifiedTime, ticksToDate } from \"@rewind/osu-local/utils\";\nimport { Beatmap as DBBeatmap, OsuDBReader } from \"@rewind/osu-local/db-reader\";\nimport { Inject } from \"@nestjs/common\";\nimport { OSU_FOLDER } from \"../constants\";\nimport { join } from \"path\";\n\nconst { readFile } = promises;\n\nconst mapToBlueprint = (b: DBBeatmap): BlueprintInfo => {\n  return {\n    md5Hash: b.md5Hash,\n    audioFileName: b.audioFileName,\n    folderName: b.folderName,\n    lastPlayed: ticksToDate(b.lastPlayed)[0],\n    osuFileName: b.fileName,\n    artist: b.artist, // unicode?\n    title: b.title,\n    creator: b.creator,\n  };\n};\n\nexport class OsuDBDao {\n  private lastMtime = -1;\n  private blueprints: BlueprintInfo[] = [];\n\n  constructor(@Inject(OSU_FOLDER) private readonly osuFolder: string) {}\n\n  private get osuDbPath() {\n    return join(this.osuFolder, \"osu!.db\");\n  }\n\n  private async createReader() {\n    const buffer = await readFile(this.osuDbPath);\n    return new OsuDBReader(buffer);\n  }\n\n  async getOsuDbLastModifiedTime() {\n    return await fileLastModifiedTime(this.osuDbPath);\n  }\n\n  async hasChanged(): Promise<boolean> {\n    return this.cachedTime !== (await this.getOsuDbLastModifiedTime());\n  }\n\n  get cachedTime() {\n    return this.lastMtime;\n  }\n\n  async getAllBlueprints(): Promise<BlueprintInfo[]> {\n    const lastModified = await this.getOsuDbLastModifiedTime();\n    if (lastModified === this.lastMtime) {\n      return this.blueprints;\n    }\n\n    this.lastMtime = lastModified;\n    const reader = await this.createReader();\n    const osuDB = await reader.readOsuDB();\n    return (this.blueprints = osuDB.beatmaps.map(mapToBlueprint));\n  }\n\n  async getBlueprintByMD5(md5: string): Promise<BlueprintInfo | undefined> {\n    const maps = await this.getAllBlueprints();\n    return maps.find((m) => m.md5Hash === md5);\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/config/DesktopConfigController.ts",
    "content": "import { Body, Controller, Get, Logger, Post, Res } from \"@nestjs/common\";\nimport { DesktopConfigService } from \"./DesktopConfigService\";\nimport { Response } from \"express\";\nimport { osuFolderSanityCheck } from \"./utils\";\n\ninterface UpdateOsuStablePathDto {\n  osuStablePath: string;\n}\n\n@Controller(\"/desktop\")\nexport class DesktopConfigController {\n  private logger = new Logger(DesktopConfigController.name);\n\n  constructor(private readonly desktopConfigService: DesktopConfigService) {}\n\n  @Post()\n  async saveOsuStablePath(@Res() res: Response, @Body() { osuStablePath }: UpdateOsuStablePathDto) {\n    this.logger.log(`Received request to update the OsuStablePath to ${osuStablePath}`);\n\n    const sanityCheckPassed = await osuFolderSanityCheck(osuStablePath);\n    if (sanityCheckPassed) {\n      await this.desktopConfigService.saveOsuStablePath(osuStablePath);\n      res.status(200).json({ result: \"OK\" });\n    } else {\n      res.status(400).json({ error: `Given folder '${osuStablePath}' does not seem to be a valid osu!stable folder` });\n    }\n  }\n\n  @Get()\n  async readConfig(@Res() res: Response) {\n    const data = await this.desktopConfigService.loadConfig();\n    res.json(data);\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/config/DesktopConfigService.ts",
    "content": "import { Inject, Injectable } from \"@nestjs/common\";\nimport { writeFile, readFile } from \"fs/promises\";\n\n/**\n * Contains essential configurations whose absence would prevent the application from working properly.\n */\nexport interface RewindDesktopConfig {\n  osuPath: string;\n}\n\n/**\n * Really cool heuristic to detect the osu folder:\n * https://github.com/Piotrekol/CollectionManager/blob/cb870d363d593035c97dc65f316a93f2d882c98b/CollectionManagerDll/Modules/FileIO/OsuPathResolver.cs#L36\n *\n */\n\nconst defaultConfig: RewindDesktopConfig = Object.freeze({\n  osuPath: \"\",\n});\n\n//\n// https://osu.ppy.sh/wiki/en/osu%21_Program_Files\n// https://github.com/sindresorhus/ps-list\n\nexport const REWIND_CFG_PATH = Symbol(\"REWIND_CONFIG_PATH\");\n\n// TODO: Use something more sophisticated with validation and so on\n\n@Injectable()\nexport class DesktopConfigService {\n  constructor(@Inject(REWIND_CFG_PATH) private readonly userConfigPath: string) {}\n\n  async loadConfig(): Promise<RewindDesktopConfig> {\n    try {\n      const data = await readFile(this.userConfigPath, \"utf-8\");\n      return JSON.parse(data);\n    } catch (err) {\n      return defaultConfig;\n    }\n  }\n\n  async saveOsuStablePath(osuStableFolderPath: string) {\n    const data: RewindDesktopConfig = {\n      osuPath: osuStableFolderPath,\n    };\n\n    return writeFile(this.userConfigPath, JSON.stringify(data));\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/config/UserConfigService.ts",
    "content": "import { Injectable } from \"@nestjs/common\";\n\ninterface Config {\n  skinId: string;\n  // apiKey: string;\n}\n\nconst NO_SKIN_ID = \"\";\n\nconst defaultConfig: Config = {\n  skinId: NO_SKIN_ID,\n  // apiKey: \"\",\n};\n\n@Injectable()\nexport class UserConfigService {\n  private readonly config: Config;\n\n  constructor() {\n    this.config = defaultConfig;\n  }\n\n  getConfig() {\n    return this.config;\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/config/utils.ts",
    "content": "/**\n * Checks if booting with the given osu! folder path would cause major errors.\n * Usually, if osu! stable does not have a problem with that folder then this API won't either.\n **/\nimport { constants } from \"fs\";\nimport { join } from \"path\";\nimport { access } from \"fs/promises\";\nimport { Logger } from \"@nestjs/common\";\n\nconst filesToCheck = [\"osu!.db\", \"scores.db\", \"Replays\", \"Skins\"];\n\n/**\n * Checks certain files to see if Rewind can be booted without any problems with the given `osuFolderPath`.\n * @param osuFolderPath the folder path to check the files in\n */\nexport async function osuFolderSanityCheck(osuFolderPath: string) {\n  try {\n    await Promise.all(filesToCheck.map((f) => access(join(osuFolderPath, f), constants.R_OK)));\n  } catch (err) {\n    Logger.log(err);\n    return false;\n  }\n  return true;\n}\n"
  },
  {
    "path": "apps/web/backend/src/constants.ts",
    "content": "// Dependency injection types\nexport const OSU_FOLDER = \"OSU_FOLDER\";\nexport const OSU_SONGS_FOLDER = Symbol(\"OSU_SONGS_FOLDER\");\n\nexport const DEFAULT_SKIN_ID = \"RewindDefault\";\n"
  },
  {
    "path": "apps/web/backend/src/environments/environment.prod.ts",
    "content": "export const environment = {\n  production: true,\n};\n"
  },
  {
    "path": "apps/web/backend/src/environments/environment.ts",
    "content": "export const environment = {\n  production: false,\n  // Where the .cfg file is stored as well\n  userDataPath: \"./tmp\",\n  // The parent directory of user data I think\n  appDataPath: \"./tmp\",\n  appResourcesPath: \"./tmp\",\n  logDirectory: \"./tmp\",\n};\n"
  },
  {
    "path": "apps/web/backend/src/events/Events.ts",
    "content": "import { Replay } from \"node-osr\";\n\nexport const ReplayWatchEvents = {\n  ReplayRead: Symbol(\"ReplayDetected\"),\n  Ready: Symbol(\"Ready\"),\n};\n\nexport interface ReplayReadEvent {\n  payload: {\n    replay: Replay;\n    filename: string;\n    // Needed?\n    path: string;\n  };\n}\n"
  },
  {
    "path": "apps/web/backend/src/events/EventsGateway.ts",
    "content": "import { Logger } from \"@nestjs/common\";\nimport { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from \"@nestjs/websockets\";\nimport { Server, Socket } from \"socket.io\";\nimport { OnEvent } from \"@nestjs/event-emitter\";\nimport { ReplayReadEvent, ReplayWatchEvents } from \"./Events\";\nimport { LocalBlueprintService } from \"../blueprints/LocalBlueprintService\";\n\n// https://gabrieltanner.org/blog/nestjs-realtime-chat\n\n@WebSocketGateway({ cors: true })\nexport class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {\n  @WebSocketServer() private server: Server;\n\n  constructor(private readonly blueprintService: LocalBlueprintService) {}\n\n  private logger: Logger = new Logger(EventsGateway.name);\n\n  afterInit(server: Server) {\n    this.logger.log(\"WebSocket Gateway initialized\");\n  }\n\n  @OnEvent(ReplayWatchEvents.ReplayRead)\n  async handleReplayAdded(event: ReplayReadEvent) {\n    const { replay, filename } = event.payload;\n    const blueprintMetaData = await this.blueprintService.getBlueprintByMD5(replay.beatmapMD5);\n\n    this.logger.log(\"Replay added \", replay.replayMD5);\n    this.server.emit(\"replayAdded\", { filename }, blueprintMetaData);\n  }\n\n  handleDisconnect(client: Socket) {\n    this.logger.log(`Client disconnected: ${client.id}`);\n  }\n\n  handleConnection(client: Socket, ...args: any[]) {\n    this.logger.log(`Client connected: ${client.id}`);\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/main.ts",
    "content": "import { environment } from \"./environments/environment\";\nimport { bootstrapRewindDesktopBackend, RewindBootstrapSettings } from \"./DesktopAPI\";\n\nconst settings: RewindBootstrapSettings = {\n  userDataPath: environment.userDataPath,\n  appDataPath: environment.appDataPath,\n  appResourcesPath: environment.appResourcesPath,\n  logDirectory: environment.logDirectory,\n};\n\nvoid bootstrapRewindDesktopBackend(settings);\n"
  },
  {
    "path": "apps/web/backend/src/replays/LocalReplayController.ts",
    "content": "import { Controller, Get, Logger, Param, Res } from \"@nestjs/common\";\nimport { Response } from \"express\";\nimport { LocalReplayService } from \"./LocalReplayService\";\n\n/**\n * This is technically just a temporary solution because parsing the .osr file on a web browser is a challenge that I\n * want to handle in the future.\n *\n *\n * A replay name has the following syntax:\n * [NAMESPACE]:[NAME]\n *\n * Each namespace has a different resolver that can resolve the replay.\n */\n@Controller(\"replays\")\nexport class LocalReplayController {\n  private logger = new Logger(LocalReplayController.name);\n\n  constructor(private localReplayService: LocalReplayService) {}\n\n  @Get(\":name\")\n  async decodeReplay(@Res() res: Response, @Param(\"name\") encodedName: string) {\n    const name = decodeURIComponent(encodedName);\n    this.logger.log(`Received request to decode replay '${name}'`);\n\n    const replay = await this.localReplayService.decodeReplay(name);\n    res.json(replay);\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/replays/LocalReplayService.ts",
    "content": "import { Inject, Injectable, Logger } from \"@nestjs/common\";\nimport { join } from \"path\";\nimport { read as readOsr } from \"node-osr\";\nimport { OSU_FOLDER } from \"../constants\";\nimport { splitByFirstOccurrence } from \"../utils/names\";\n\nconst REPLAY_NAME_SEPARATOR = \":\";\n\n@Injectable()\nexport class LocalReplayService {\n  private logger = new Logger(LocalReplayService.name);\n\n  constructor(@Inject(OSU_FOLDER) private osuDirectory: string) {}\n\n  exportedPath(fileName: string) {\n    return join(this.osuDirectory, \"Replays\", fileName);\n  }\n\n  internalPath(fileName: string) {\n    return join(this.osuDirectory, \"Data\", \"r\", fileName);\n  }\n\n  /**\n   * osu!/Replays\n   */\n  async osuExportedReplay(fileName: string) {\n    return await readOsr(this.exportedPath(fileName));\n  }\n\n  /**\n   * osu!/Data/r\n   */\n  async osuInternalReplay(fileName: string) {\n    return await readOsr(this.internalPath(fileName));\n  }\n\n  /**\n   * Found in the local file system\n   */\n  async localReplay(absoluteFilePath: string) {\n    return await readOsr(absoluteFilePath);\n  }\n\n  async decodeReplay(name: string) {\n    const [nameSpace, id] = splitByFirstOccurrence(name, REPLAY_NAME_SEPARATOR);\n    switch (nameSpace) {\n      case \"exported\":\n        return this.osuExportedReplay(id);\n      case \"internal\":\n        return this.osuInternalReplay(id);\n      case \"local\":\n        return this.localReplay(id);\n      case \"api\":\n      // Maybe?\n    }\n    return Promise.resolve(undefined);\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/replays/ReplayWatcher.ts",
    "content": "import * as chokidar from \"chokidar\";\nimport { basename, join } from \"path\";\nimport { EventEmitter2 } from \"@nestjs/event-emitter\";\nimport { read as osrRead } from \"node-osr\";\nimport { Injectable, Logger } from \"@nestjs/common\";\nimport { ReplayReadEvent, ReplayWatchEvents } from \"../events/Events\";\n\n@Injectable()\nexport class ReplayWatcher {\n  // Can be used for osu!/Data/r/ and osu!/Replays\n  private logger = new Logger(ReplayWatcher.name);\n\n  constructor(private readonly eventEmitter: EventEmitter2) {}\n\n  // TODO: onModuleInit -> ok maybe not here , but something that also takes folders to watch\n\n  watchForReplays(replaysFolder: string): ReplayWatcher {\n    // ignoreInitial must be true otherwise addDir will be triggered for every folder initially.\n    const globPattern = join(replaysFolder);\n    this.logger.log(`Watching for replays (.osr) in folder: ${replaysFolder} with pattern: ${globPattern}`);\n    const watcher = chokidar.watch(globPattern, {\n      ignoreInitial: true,\n      persistent: true,\n      depth: 0, // if somehow osu! is trolling, this will prevent it\n    });\n    watcher.on(\"ready\", () => {\n      this.eventEmitter.emit(ReplayWatchEvents.Ready);\n    });\n    watcher.on(\"add\", (path) => {\n      if (!path.endsWith(\".osr\")) {\n        return;\n      }\n      this.logger.log(`Detected path: ${path}`);\n      osrRead(path, (err, replay) => {\n        const event: ReplayReadEvent = { payload: { replay, path, filename: basename(path) } };\n        this.eventEmitter.emit(ReplayWatchEvents.ReplayRead, event);\n      });\n    });\n    return this;\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/replays/ScoresDBDao.ts",
    "content": "//\n// import { Score as DBScore, ScoresDBReader } from \"osu-db-parser\";\n// import { readFile } from \"fs/promises\";\n//\n// import { legacyReplayFileName } from \"../utils/stable\";\n// import { ticksToDate } from \"../utils/dates\";\n// import { fileLastModifiedTime } from \"../utils/files\";\n// import { LocalScore } from \"../models/LocalScore\";\n//\n// const mapDBScores = (s: DBScore): LocalScore => {\n//   return {\n//     replayMD5: s.replayMD5,\n//     timestamp: ticksToDate(s.timestamp)[0],\n//     windowsTimeStamp: s.timestamp,\n//     localFileName: legacyReplayFileName(s.beatmapMD5, s.timestamp),\n//     onlineScoreId: s.onlineScoreId,\n//   };\n// };\n//\n// // In osu!lazer the implementation will differ again\n// export class ScoresDBDao {\n//   private lastMtime = -1;\n//   private scores: LocalScore[] = [];\n//   private scoresByBeatmap: Map<string, LocalScore[]> = new Map<string, LocalScore[]>();\n//\n//   constructor(private readonly scoresDBPath: string) {}\n//\n//   private async createReader() {\n//     const buffer = await readFile(this.scoresDBPath);\n//     return new ScoresDBReader(buffer);\n//   }\n//\n//   async getAllScores(force?: boolean): Promise<LocalScore[]> {\n//     const lastModified = await fileLastModifiedTime(this.scoresDBPath);\n//     if (lastModified === this.lastMtime && !force) {\n//       return this.scores;\n//     }\n//     this.lastMtime = lastModified;\n//     const reader = await this.createReader();\n//\n//     this.scores = [];\n//     this.scoresByBeatmap.clear();\n//\n//     reader.readScoresDB().beatmaps.forEach((b) => {\n//       const s = b.scores.map(mapDBScores);\n//       this.scoresByBeatmap.set(b.md5hash, s);\n//       this.scores.push(...s);\n//     });\n//     return this.scores;\n//   }\n//\n//   /**\n//    * @param replayMD5\n//    */\n//   async getScoreByReplayMD5(replayMD5: string): Promise<LocalScore | undefined> {\n//     const scores = await this.getAllScores();\n//     return scores.find((s) => s.replayMD5 === replayMD5);\n//   }\n//\n//   async getScoreByBeatmapAndReplayMD5(beatmapMD5: string, replayMD5: string): Promise<LocalScore | undefined> {\n//     await this.getAllScores();\n//     const scores = this.scoresByBeatmap.get(beatmapMD5);\n//     return scores && scores.find((s) => s.replayMD5 === replayMD5);\n//   }\n//\n//   async getScoresByBeatmapMD5(beatmapMD5: string): Promise<LocalScore[] | undefined> {\n//     await this.getAllScores();\n//     return this.scoresByBeatmap.get(beatmapMD5);\n//   }\n// }\n"
  },
  {
    "path": "apps/web/backend/src/skins/SkinController.ts",
    "content": "import { Controller, Get, Logger, Query, Res } from \"@nestjs/common\";\nimport { Response } from \"express\";\nimport { SkinService } from \"./SkinService\";\n\n@Controller(\"skins\")\nexport class SkinController {\n  private logger = new Logger(SkinController.name);\n\n  constructor(private readonly skinService: SkinService) {}\n\n  @Get(\"/list\")\n  async getAllSkins(@Res() res: Response) {\n    const skins = await this.skinService.listAllSkins();\n    res.json(skins);\n  }\n\n  @Get()\n  async getSkinInfo(@Res() res: Response, @Query() query: { hd: number; animated: number; name: string }) {\n    // We can take these in case user config does not exist\n    const { hd, animated, name } = query;\n    const hdIfExists = hd === 1;\n    const animatedIfExists = animated === 1;\n    const decodedName = decodeURIComponent(name);\n    this.logger.log(`Skin requested ${decodedName} with hd=${hdIfExists} animated=${animatedIfExists}`);\n    // TODO: Inject these parameters ...\n    const info = await this.skinService.getSkinInfo(decodedName);\n    res.json(info);\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/skins/SkinNameResolver.ts",
    "content": "import { Inject, Injectable } from \"@nestjs/common\";\nimport { join } from \"path\";\n\ninterface SkinsFolderConfig {\n  prefix: string;\n  path: string;\n}\n\nexport type SkinNameResolverConfig = SkinsFolderConfig[];\n\nexport const SKIN_NAME_RESOLVER_CONFIG = \"SkinsFolderConfig\";\n\n@Injectable()\nexport class SkinNameResolver {\n  constructor(@Inject(SKIN_NAME_RESOLVER_CONFIG) private readonly skinNameResolverConfig: SkinNameResolverConfig) {}\n\n  resolveNameToPath(name: string) {\n    const [source, folder] = name.split(\"/\");\n    const folderPath = this.skinNameResolverConfig.find((c) => c.prefix === source);\n    if (folderPath === undefined) {\n      throw Error(`Source='${source}' was not configured.`);\n    }\n    return { source, name: folder, path: join(folderPath.path, folder) };\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/skins/SkinService.ts",
    "content": "import { Inject, Injectable, Logger } from \"@nestjs/common\";\nimport { GetTextureFileOption, OsuSkinTextureResolver, SkinFolderReader } from \"@rewind/osu-local/skin-reader\";\nimport { SkinNameResolver } from \"./SkinNameResolver\";\nimport { DEFAULT_SKIN_TEXTURE_CONFIG, OsuSkinTextures } from \"@rewind/osu/skin\";\nimport { OSU_FOLDER } from \"../constants\";\nimport { join } from \"path\";\n\nconst OSU_DEFAULT_SKIN_ID = \"rewind/OsuDefaultSkin\";\n\n@Injectable()\nexport class SkinService {\n  private logger = new Logger(SkinService.name);\n\n  constructor(@Inject(OSU_FOLDER) private osuDirectory: string, private readonly skinNameResolver: SkinNameResolver) {}\n\n  async listAllSkins() {\n    return SkinFolderReader.listSkinsInFolder(join(this.osuDirectory, \"Skins\"), { skinIniRequired: false });\n  }\n\n  /**\n   * In the future we also want to get the skin info with the beatmap as the parameters in order to retrieve\n   * beatmap related skin files as well.\n   */\n\n  async resolve(\n    osuSkinTexture: OsuSkinTextures,\n    options: GetTextureFileOption,\n    list: { prefix: string; resolver: OsuSkinTextureResolver }[],\n  ) {\n    for (const { prefix, resolver } of list) {\n      const filePaths = await resolver.resolve(osuSkinTexture, options);\n      if (filePaths.length === 0) {\n        continue;\n      }\n      return filePaths.map((path) => `${prefix}/${path}`);\n    }\n    this.logger.warn(`No skin has the skin texture ${osuSkinTexture}`);\n    return [];\n  }\n\n  /**\n   * @param skinName will be resolved through the given skin name resolver.\n   */\n  async getSkinInfo(skinName: string) {\n    this.logger.log(`Getting skin info for name=${skinName}`);\n\n    const resolved = this.skinNameResolver.resolveNameToPath(skinName);\n    if (resolved === null) {\n      // Maybe error will be logged through middleware?\n      throw new Error(`Skin ${skinName} could not be found in the folders`);\n    }\n    const { name, path, source } = resolved;\n\n    const osuDefaultSkinResolver = await SkinFolderReader.getSkinResolver(\n      this.skinNameResolver.resolveNameToPath(OSU_DEFAULT_SKIN_ID).path,\n    );\n    const skinResolver = await SkinFolderReader.getSkinResolver(path);\n    // TODO: Include default osu! skin\n    // const defaultSkinResolver = await SkinFolderReader.getSkinResolver(\"\");\n    const { config } = skinResolver;\n\n    // TODO: Give through params\n    const option = { hdIfExists: true, animatedIfExists: true };\n\n    // const files = await skinResolver.resolveAllTextureFiles({ hdIfExists, animatedIfExists });\n    const skinTextureKeys = Object.keys(DEFAULT_SKIN_TEXTURE_CONFIG);\n    const files = await Promise.all(\n      skinTextureKeys.map(async (key) => ({\n        key,\n        paths: await this.resolve(key as OsuSkinTextures, option, [\n          // In the future beatmap stuff can be listed here as well\n          {\n            prefix: [\"static\", \"skins\", source, name].map(encodeURIComponent).join(\"/\"),\n            resolver: skinResolver,\n          },\n          {\n            prefix: [\"static\", \"skins\", \"rewind\", \"OsuDefaultSkin\"].map(encodeURIComponent).join(\"/\"),\n            resolver: osuDefaultSkinResolver,\n          },\n          // {\n          //   prefix: `/static/skins/${encodeURIComponent(skinName)}`,\n          //   resolver: defaultSkinResolver,\n          // },\n        ]),\n      })),\n    );\n\n    return { config, files };\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/status/SetupStatusController.ts",
    "content": "import { Controller, Get, Res } from \"@nestjs/common\";\nimport { Response } from \"express\";\n\n@Controller(\"/status\")\nexport class SetupStatusController {\n  @Get()\n  getBackendStatus(@Res() res: Response) {\n    res.json({ status: \"SETUP_MISSING\" });\n  }\n}\n\n@Controller(\"/status\")\nexport class NormalStatusController {\n  // TODO: ...\n  @Get()\n  getBackendStatus(@Res() res: Response) {\n    res.json({ status: \"READY\" });\n  }\n}\n"
  },
  {
    "path": "apps/web/backend/src/utils/names.spec.ts",
    "content": "import { splitByFirstOccurrence } from \"./names\";\n\ntest(\"splitByFirstOccurrence\", () => {\n  expect(splitByFirstOccurrence(\"a:b\", \":\")).toEqual([\"a\", \"b\"]);\n  expect(splitByFirstOccurrence(\"a:b:c\", \":\")).toEqual([\"a\", \"b:c\"]);\n  expect(splitByFirstOccurrence(\"a\", \":\")).toEqual([\"a\"]);\n});\n"
  },
  {
    "path": "apps/web/backend/src/utils/names.ts",
    "content": "/**\n * Utility function to split the given string `str` into *at most two* parts, but only at the first occurrence of the\n * given delimiter. For example \"a:b:c\" splitting with delimiter=\":\" will result into [\"a\", \"b:c\"].\n *\n * @param str the string to split\n * @param delimiter the delimiter\n */\nexport function splitByFirstOccurrence(str: string, delimiter: string) {\n  const [first, ...rest] = str.split(delimiter);\n  if (rest.length === 0) return [first];\n  return [first, rest.join(delimiter)];\n}\n"
  },
  {
    "path": "apps/web/backend/tsconfig.app.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"node\"\n    ],\n    \"emitDecoratorMetadata\": true,\n    \"target\": \"es2015\"\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "apps/web/backend/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "apps/web/backend/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\"jest\", \"node\"]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "babel.config.json",
    "content": "{\n  \"babelrcRoots\": [\"*\"]\n}\n"
  },
  {
    "path": "electron-builder.json",
    "content": "{\n  \"appId\": \"sh.abstrakt.rewind\",\n  \"productName\": \"Rewind\",\n  \"nsis\": {\n    \"oneClick\": false,\n    \"allowToChangeInstallationDirectory\": true\n  },\n  \"generateUpdatesFilesForAllChannels\": true,\n  \"directories\": {\n    \"output\": \"dist/electron\",\n    \"buildResources\": \"tools/electron-builder/build-resources\",\n    \"app\": \".\"\n  },\n  \"snap\": {\n    \"publish\":  [\"github\"]\n  },\n  \"files\": [\n    {\n      \"from\": \"dist/apps/desktop\",\n      \"filter\": [\n        \"index.js\",\n        \"assets\"\n      ]\n    },\n    {\n      \"from\": \"dist/apps/desktop/frontend\",\n      \"to\": \"frontend\"\n    },\n    {\n      \"from\": \"dist/apps/desktop/backend\",\n      \"to\": \"backend\"\n    },\n    \"package.json\"\n  ],\n  \"extraResources\": [\n    {\n      \"from\": \"resources/Skins\",\n      \"to\": \"Skins\"\n    }\n  ]\n}\n"
  },
  {
    "path": "jest.config.ts",
    "content": "const { getJestProjects } = require(\"@nrwl/jest\");\n\nexport default {\n  projects: getJestProjects(),\n  // Make the environment variables in `.env` accessible through `process.env.*`\n  // TODO: For some reason setting this in the global jest.config.js doesn't work and we need to use this in the child\n  // config files\n  setupFiles: [\"dotenv/config\"],\n};\n"
  },
  {
    "path": "jest.preset.js",
    "content": "const nxPreset = require(\"@nrwl/jest/preset\").default;\n\nmodule.exports = { ...nxPreset };\n"
  },
  {
    "path": "libs/@types/node-osr/index.d.ts",
    "content": "// Type definitions for node-osr\n// Project: Node OSR Reader\n// Definitions by: abstrakt\n\nexport class Replay {\n  gameMode: number;\n  gameVersion: number;\n  beatmapMD5: string;\n  playerName: string;\n  replayMD5: string;\n  number_300s: number;\n  number_100s: number;\n  number_50s: number;\n  gekis: number;\n  katus: number;\n  misses: number;\n  score: number;\n  max_combo: number;\n  perfect_combo: number;\n  mods: number;\n  life_bar: string;\n  timestamp: number;\n  replay_length: number;\n  replay_data: string;\n\n  serializeSync(): Buffer;\n\n  serialize(): Promise<Buffer>;\n\n  writeSync(path: string);\n\n  write(path: string, cb?: any);\n}\n\n/**\n * @param input either the filename to read or a buffer\n */\nexport function read(input: string | Buffer): Promise<Replay>;\n/**\n *\n * @param input either the filename to read or a buffer\n * @param cb the callback\n * @private\n */\nexport function read(input: string | Buffer, cb?: (err: Error, replay: Replay) => unknown): void;\n\n/**\n * @param input either the filename to read or a buffer\n */\nexport function readSync(input: string | Buffer): Replay;\n"
  },
  {
    "path": "libs/osu/core/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu/core/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/core/README.md",
    "content": "Gameplay related functionalities are implemented in this package.\n\nUnit tests are found inside the `src` while integration tests are in the `test` folder.\n"
  },
  {
    "path": "libs/osu/core/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-core\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu/core\",\n};\n"
  },
  {
    "path": "libs/osu/core/package.json",
    "content": "{\n  \"name\": \"@osujs/core\",\n  \"version\": \"0.0.4\"\n}\n"
  },
  {
    "path": "libs/osu/core/src/audio/HitSampleInfo.ts",
    "content": "export class HitSampleInfo {\n  static HIT_WHISTLE = \"hitwhistle\";\n  static HIT_FINISH = \"hitfinish\";\n  static HIT_NORMAL = \"hitnormal\";\n  static HIT_CLAP = \"hitclap\";\n\n  volume = 0;\n  lookupNames?: IterableIterator<string>;\n\n  static ALL_ADDITIONS = [HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_FINISH];\n\n  // name could be one of those HIT_WHISTLE, ...\n  name = \"\";\n  // sample set such as drum, normal, soft\n  bank: string | null;\n  suffix: string | null;\n\n  constructor(name: string, bank: string | null = null, suffix: string | null = null, volume = 0) {\n    this.name = name;\n    this.bank = bank;\n    this.suffix = suffix;\n    this.volume = volume;\n  }\n\n  // with() for overriding\n}\n"
  },
  {
    "path": "libs/osu/core/src/audio/LegacySampleBank.ts",
    "content": "export enum LegacySampleBank {\n  None = 0,\n  Normal = 1,\n  Soft = 2,\n  Drum = 3,\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/Beatmap.ts",
    "content": "import { BeatmapDifficulty, DEFAULT_BEATMAP_DIFFICULTY } from \"./BeatmapDifficulty\";\nimport { determineDefaultPlaybackSpeed, normalizeHitObjects } from \"../utils\";\nimport { Slider } from \"../hitobjects/Slider\";\nimport { HitCircle } from \"../hitobjects/HitCircle\";\nimport { OsuClassicMod } from \"../mods/Mods\";\nimport { Spinner } from \"../hitobjects/Spinner\";\nimport { SliderCheckPoint } from \"../hitobjects/SliderCheckPoint\";\nimport { AllHitObjects, isHitCircle, OsuHitObject } from \"../hitobjects/Types\";\nimport { TimingControlPoint } from \"./ControlPoints/TimingControlPoint\";\nimport { ControlPointInfo } from \"./ControlPoints/ControlPointInfo\";\n\n/**\n * A built beatmap that is not supposed to be modified.\n */\nexport class Beatmap {\n  static EMPTY_BEATMAP = new Beatmap([], DEFAULT_BEATMAP_DIFFICULTY, [], new ControlPointInfo());\n\n  private readonly hitObjectDict: Record<string, AllHitObjects>;\n  public readonly gameClockRate: number;\n\n  constructor(\n    public readonly hitObjects: Array<OsuHitObject>,\n    public readonly difficulty: BeatmapDifficulty,\n    public readonly appliedMods: OsuClassicMod[],\n    public readonly controlPointInfo: ControlPointInfo,\n  ) {\n    this.hitObjectDict = normalizeHitObjects(hitObjects);\n    this.gameClockRate = determineDefaultPlaybackSpeed(appliedMods);\n  }\n\n  getHitObject(id: string): AllHitObjects {\n    return this.hitObjectDict[id];\n  }\n\n  // TODO: Perform some .type checks otherwise these don't make sense\n\n  getSliderCheckPoint(id: string): SliderCheckPoint {\n    return this.hitObjectDict[id] as SliderCheckPoint;\n  }\n\n  getSlider(id: string): Slider {\n    return this.hitObjectDict[id] as Slider;\n  }\n\n  getHitCircle(id: string): HitCircle {\n    return this.hitObjectDict[id] as HitCircle;\n  }\n\n  getSpinner(id: string): Spinner {\n    return this.hitObjectDict[id] as Spinner;\n  }\n}\n\n// Utility?\nconst endTime = (o: OsuHitObject) => (isHitCircle(o) ? o.hitTime : o.endTime);\n\nexport function mostCommonBeatLength({\n                                       hitObjects,\n                                       timingPoints,\n                                     }: { hitObjects: OsuHitObject[], timingPoints: TimingControlPoint[] }) {\n  // The last playable time in the beatmap - the last timing point extends to this time.\n  // Note: This is more accurate and may present different results because osu-stable didn't have the ability to\n  // calculate slider durations in this context.\n  let lastTime = 0;\n  if (hitObjects.length > 0)\n    lastTime = endTime(hitObjects[hitObjects.length - 1]);\n  else if (timingPoints.length > 0)\n    lastTime = timingPoints[timingPoints.length - 1].time;\n\n  // 1. Group the beat lengths and aggregate the durations\n  const durations: Map<number, number> = new Map<number, number>();\n\n  function add(d: number, x: number) {\n    d = Math.round(d * 1000) / 1000;\n    const a = durations.get(d);\n    if (a === undefined)\n      durations.set(d, x);\n    else\n      durations.set(d, a + x);\n  }\n\n  for (let i = 0; i < timingPoints.length; i++) {\n    const t = timingPoints[i];\n    if (t.time > lastTime) {\n      add(t.beatLength, 0);\n    } else {\n      // osu-stable forced the first control point to start at 0.\n      // This is reproduced here to maintain compatibility around osu!mania scroll speed and song select display.\n      const currentTime = i === 0 ? 0 : t.time;\n      const nextTime = i + 1 === timingPoints.length ? lastTime : timingPoints[i + 1].time;\n      add(t.beatLength, nextTime - currentTime);\n    }\n  }\n\n  // 2. Sort by duration descendingly\n\n  const list: Array<{ beatLength: number, duration: number }> = [];\n  for (const beatLength of durations.keys()) {\n    list.push({ beatLength: beatLength, duration: durations.get(beatLength) as number });\n  }\n\n  list.sort((a, b) => b.duration - a.duration);\n\n  if (list.length === 0) {\n    return undefined;\n  } else {\n    return list[0].beatLength;\n  }\n}\n\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/BeatmapBuilder.ts",
    "content": "import { Blueprint } from \"../blueprint/Blueprint\";\nimport { BeatmapDifficulty } from \"./BeatmapDifficulty\";\nimport { HitCircleSettings, HitObjectSettings, SliderSettings, SpinnerSettings } from \"../blueprint/HitObjectSettings\";\nimport { ControlPointInfo } from \"./ControlPoints/ControlPointInfo\";\nimport { HitCircle } from \"../hitobjects/HitCircle\";\nimport { BeatmapDifficultyAdjuster, ModSettings, OsuClassicMod } from \"../mods/Mods\";\nimport { modifyStackingPosition } from \"../mods/StackingMod\";\nimport { generateSliderCheckpoints } from \"../hitobjects/slider/SliderCheckPointGenerator\";\nimport { SliderCheckPointDescriptor } from \"../hitobjects/slider/SliderCheckPointDescriptor\";\nimport { SliderPath } from \"../hitobjects/slider/SliderPath\";\nimport { TimingControlPoint } from \"./ControlPoints/TimingControlPoint\";\nimport { Slider } from \"../hitobjects/Slider\";\nimport { SliderCheckPoint } from \"../hitobjects/SliderCheckPoint\";\nimport { Beatmap } from \"./Beatmap\";\nimport { Spinner } from \"../hitobjects/Spinner\";\nimport { approachRateToApproachDuration, circleSizeToScale, Position } from \"@osujs/math\";\nimport { OsuHitObject } from \"../hitobjects/Types\";\nimport { HardRockMod } from \"../mods/HardRockMod\";\nimport { PathControlPoint } from \"../hitobjects/slider/PathControlPoint\";\n\nfunction copyPosition({ x, y }: Position): Position {\n  return { x, y };\n}\n\nfunction createHitCircle(\n  id: string,\n  hitCircleSettings: HitCircleSettings,\n  controlPointInfo: ControlPointInfo,\n  beatmapDifficulty: BeatmapDifficulty,\n): HitCircle {\n  const hitCircle = new HitCircle();\n  hitCircle.id = id;\n  hitCircle.position = copyPosition(hitCircleSettings.position);\n  hitCircle.unstackedPosition = copyPosition(hitCircleSettings.position);\n  hitCircle.hitTime = hitCircleSettings.time;\n  hitCircle.scale = circleSizeToScale(beatmapDifficulty.circleSize);\n  hitCircle.approachDuration = approachRateToApproachDuration(beatmapDifficulty.approachRate);\n  return hitCircle;\n}\n\nfunction createSliderCheckPoint(slider: Slider, id: string, descriptor: SliderCheckPointDescriptor) {\n  const checkPoint = new SliderCheckPoint(slider);\n  const { time, spanStartTime, spanIndex, spanProgress } = descriptor;\n  checkPoint.id = id;\n  checkPoint.offset = slider.path.positionAt(spanProgress);\n  checkPoint.type = descriptor.type;\n  checkPoint.hitTime = time;\n  checkPoint.spanIndex = spanIndex;\n  checkPoint.spanStartTime = spanStartTime;\n  checkPoint.spanProgress = spanProgress;\n  return checkPoint;\n}\n\nfunction createSliderCheckPoints(slider: Slider): SliderCheckPoint[] {\n  const checkPoints: SliderCheckPoint[] = [];\n  let checkpointIndex = 0;\n  for (const e of generateSliderCheckpoints(\n    slider.startTime,\n    slider.spanDuration,\n    slider.velocity,\n    slider.tickDistance,\n    slider.path.distance,\n    slider.spanCount,\n    slider.legacyLastTickOffset,\n  )) {\n    const id = `${slider.id}/${checkpointIndex++}`;\n    checkPoints.push(createSliderCheckPoint(slider, id, e));\n  }\n  return checkPoints;\n}\n\nfunction copyPathPoints(pathPoints: PathControlPoint[]) {\n  return pathPoints.map(({ type, offset }) => ({\n    type,\n    offset: copyPosition(offset),\n  }));\n}\n\nfunction createSlider(\n  index: number,\n  sliderSettings: SliderSettings,\n  controlPointInfo: ControlPointInfo,\n  difficulty: BeatmapDifficulty,\n): Slider {\n  const approachDuration = approachRateToApproachDuration(difficulty.approachRate);\n  const scale = circleSizeToScale(difficulty.circleSize);\n  const hitTime = sliderSettings.time;\n  const timingPoint: TimingControlPoint = controlPointInfo.timingPointAt(hitTime);\n  const difficultyPoint = controlPointInfo.difficultyPointAt(hitTime);\n  const scoringDistance = Slider.BASE_SCORING_DISTANCE * difficulty.sliderMultiplier * difficultyPoint.speedMultiplier;\n  const sliderId = index.toString();\n\n  const head = new HitCircle();\n  head.id = `${index.toString()}/HEAD`;\n  head.unstackedPosition = copyPosition(sliderSettings.position);\n  head.position = copyPosition(sliderSettings.position);\n  head.hitTime = sliderSettings.time;\n  head.approachDuration = approachDuration;\n  head.scale = scale;\n  head.sliderId = sliderId;\n\n  const slider = new Slider(head);\n  slider.id = sliderId;\n  slider.repeatCount = sliderSettings.repeatCount;\n  slider.legacyLastTickOffset = sliderSettings.legacyLastTickOffset;\n  slider.velocity = scoringDistance / timingPoint.beatLength;\n  slider.tickDistance = (scoringDistance / difficulty.sliderTickRate) * sliderSettings.tickDistanceMultiplier;\n  slider.path = new SliderPath(copyPathPoints(sliderSettings.pathPoints), sliderSettings.length);\n  slider.checkPoints = createSliderCheckPoints(slider);\n  return slider;\n}\n\nfunction createSpinner(\n  id: string,\n  settings: SpinnerSettings,\n  controlPointInfo: ControlPointInfo,\n  difficulty: BeatmapDifficulty,\n) {\n  const spinner = new Spinner();\n  spinner.id = id;\n  spinner.startTime = settings.time;\n  spinner.duration = settings.duration;\n  return spinner;\n}\n\nfunction createStaticHitObject(\n  index: number,\n  hitObjectSetting: HitObjectSettings,\n  controlPointInfo: ControlPointInfo,\n  beatmapDifficulty: BeatmapDifficulty,\n): OsuHitObject {\n  switch (hitObjectSetting.type) {\n    case \"HIT_CIRCLE\":\n      return createHitCircle(\n        index.toString(),\n        hitObjectSetting as HitCircleSettings,\n        controlPointInfo,\n        beatmapDifficulty,\n      );\n    case \"SLIDER\":\n      return createSlider(index, hitObjectSetting as SliderSettings, controlPointInfo, beatmapDifficulty);\n    case \"SPINNER\":\n      return createSpinner(index.toString(), hitObjectSetting as SpinnerSettings, controlPointInfo, beatmapDifficulty);\n  }\n  throw new Error(\"Type not recognized...\");\n}\n\n// Mutates the hitObject combo index values\nfunction assignComboIndex(bluePrintSettings: HitObjectSettings[], hitObjects: OsuHitObject[]) {\n  let comboSetIndex = -1,\n    withinSetIndex = 0;\n  for (let i = 0; i < hitObjects.length; i++) {\n    const { newCombo, comboSkip, type } = bluePrintSettings[i];\n    const hitObject = hitObjects[i]; // change 'const' -> 'let' for better readability\n\n    if (i === 0 || newCombo || type === \"SPINNER\") {\n      comboSetIndex += comboSkip + 1;\n      withinSetIndex = 0;\n    }\n\n    // Spinners do not have comboSetIndex or withinComboSetIndex\n    if (hitObject instanceof HitCircle) {\n      hitObject.comboSetIndex = comboSetIndex;\n      hitObject.withinComboSetIndex = withinSetIndex++;\n    } else if (hitObject instanceof Slider) {\n      hitObject.head.comboSetIndex = comboSetIndex;\n      hitObject.head.withinComboSetIndex = withinSetIndex++;\n    }\n  }\n}\n\n// There should only be one, otherwise ...\nfunction findDifficultyApplier(mods: OsuClassicMod[]): BeatmapDifficultyAdjuster {\n  for (const m of mods) {\n    const adjuster = ModSettings[m].difficultyAdjuster;\n    if (adjuster !== undefined) {\n      return adjuster;\n    }\n  }\n  return (d: BeatmapDifficulty) => d; // The identity function\n}\n\ninterface BeatmapBuilderOptions {\n  addStacking: boolean;\n  mods: OsuClassicMod[];\n}\n\nconst defaultBeatmapBuilderOptions: BeatmapBuilderOptions = {\n  addStacking: true,\n  mods: [],\n};\n\n/**\n * Builds the beatmap from the given blueprint and options.\n *\n * It DOES not perform a check on the given subset of mods. So if you enter half-time and double time at the same time,\n * then this might return bad results.\n *\n * @param {Blueprint} bluePrint\n * @param {Object} options\n * @param {boolean} options.addStacking whether to apply setting or not (by default true)\n */\nexport function buildBeatmap(bluePrint: Blueprint, options?: Partial<BeatmapBuilderOptions>): Beatmap {\n  const { beatmapVersion, stackLeniency } = bluePrint.blueprintInfo;\n  const { mods, addStacking } = { ...defaultBeatmapBuilderOptions, ...options };\n\n  const finalDifficulty = findDifficultyApplier(mods)(bluePrint.defaultDifficulty);\n\n  const hitObjects: OsuHitObject[] = bluePrint.hitObjectSettings.map((setting, index) =>\n    createStaticHitObject(index, setting, bluePrint.controlPointInfo, finalDifficulty),\n  );\n\n  assignComboIndex(bluePrint.hitObjectSettings, hitObjects);\n\n  if (mods.includes(\"HARD_ROCK\")) {\n    HardRockMod.flipVertically(hitObjects);\n  }\n\n  if (addStacking) {\n    modifyStackingPosition(hitObjects, stackLeniency, beatmapVersion);\n  }\n\n  return new Beatmap(hitObjects, finalDifficulty, mods, bluePrint.controlPointInfo);\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/BeatmapDifficulty.ts",
    "content": "export type BeatmapDifficulty = {\n  drainRate: number;\n  circleSize: number;\n  overallDifficulty: number;\n  approachRate: number;\n  sliderMultiplier: number;\n  sliderTickRate: number;\n};\n\nexport const DEFAULT_BEATMAP_DIFFICULTY: BeatmapDifficulty = Object.freeze({\n  drainRate: 5,\n  circleSize: 5,\n  overallDifficulty: 5,\n  // Technically speaking default value of AR is 5 because OD is 5\n  // https://github.com/ppy/osu/blob/b1fcb840a9ff4d866aac262ace7f54fa88b5e0ce/osu.Game/Beatmaps/BeatmapDifficulty.cs#L35\n  approachRate: 5,\n  sliderMultiplier: 1,\n  sliderTickRate: 1,\n});\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/ControlPoints/ControlPoint.ts",
    "content": "import { ControlPointGroup } from \"./ControlPointGroup\";\nimport { Comparable } from \"../../utils/SortedList\";\n\nexport abstract class ControlPoint implements Comparable<ControlPoint> {\n  controlPointGroup?: ControlPointGroup;\n\n  get time(): number {\n    return this.controlPointGroup?.time ?? 0;\n  }\n\n  abstract get type(): string;\n\n  abstract isRedundant(existing: ControlPoint): boolean;\n\n  compareTo(other: ControlPoint): number {\n    return this.time - other.time;\n  }\n\n  attachGroup(pointGroup: ControlPointGroup): void {\n    this.controlPointGroup = pointGroup;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/ControlPoints/ControlPointGroup.ts",
    "content": "import { ControlPoint } from \"./ControlPoint\";\nimport { ControlPointInfo } from \"./ControlPointInfo\";\n\nexport class ControlPointGroup {\n  time = 0;\n  controlPoints: ControlPoint[] = [];\n  controlPointInfo?: ControlPointInfo;\n\n  // itemRemoved: any;\n  // itemAdded: any;\n\n  constructor(time: number) {\n    this.time = time;\n  }\n\n  add(point: ControlPoint): void {\n    const i = this.controlPoints.findIndex((value) => value.type === point.type);\n    if (i > -1) {\n      const p = this.controlPoints[i];\n      this.controlPoints.splice(i, 1);\n      this.controlPointInfo?.groupItemRemoved(p);\n    }\n    point.attachGroup(this);\n    this.controlPoints.push(point);\n\n    this.controlPointInfo?.groupItemAdded(point);\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/ControlPoints/ControlPointInfo.ts",
    "content": "import { DifficultyControlPoint } from \"./DifficultyControlPoint\";\nimport { TimingControlPoint } from \"./TimingControlPoint\";\nimport { ControlPoint } from \"./ControlPoint\";\nimport { EffectControlPoint } from \"./EffectControlPoint\";\nimport { SampleControlPoint } from \"./SampleControlPoint\";\nimport { ControlPointGroup } from \"./ControlPointGroup\";\nimport { floatEqual } from \"@osujs/math\";\nimport { SortedList } from \"../../utils/SortedList\";\n\nexport class ControlPointInfo {\n  difficultyPoints: SortedList<DifficultyControlPoint> = new SortedList<DifficultyControlPoint>();\n  timingPoints: SortedList<TimingControlPoint> = new SortedList<TimingControlPoint>();\n  effectPoints: SortedList<EffectControlPoint> = new SortedList<EffectControlPoint>();\n  samplePoints: SortedList<SampleControlPoint> = new SortedList<SampleControlPoint>();\n\n  // Why not use SortedList here ?\n  groups: ControlPointGroup[] = [];\n\n  difficultyPointAt(time: number): DifficultyControlPoint {\n    return this.binarySearchWithFallback(this.difficultyPoints.list, time, DifficultyControlPoint.DEFAULT);\n  }\n\n  samplePointAt(time: number): SampleControlPoint {\n    return this.binarySearchWithFallback(\n      this.samplePoints.list,\n      time,\n      this.samplePoints.length > 0 ? this.samplePoints.list[0] : SampleControlPoint.DEFAULT,\n    );\n  }\n\n  add(time: number, controlPoint: ControlPoint): boolean {\n    if (this.checkAlreadyExisting(time, controlPoint)) return false;\n    const g = this.groupAt(time, true) as ControlPointGroup;\n    g.add(controlPoint);\n\n    return true;\n  }\n\n  groupAt(time: number, addIfNotExisting = false): ControlPointGroup | null {\n    const newGroup = new ControlPointGroup(time);\n\n    const found = this.groups.find((o) => floatEqual(o.time, time));\n    if (found) return found;\n    if (addIfNotExisting) {\n      // this is a workaround for the following two uncommented lines\n      newGroup.controlPointInfo = this;\n      // newGroup.itemAdded = this.groupItemAdded;\n      // newGroup.itemRemoved = this.groupItemRemoved;\n      this.groups.push(newGroup);\n\n      // osu!lazer they use .insert(~i) to maintain it sorted ... -> isn't this O(n^2)?\n\n      // we sort cause lazy rn (optimize later)\n      this.groups.sort((a, b) => a.time - b.time);\n\n      return newGroup;\n    }\n    return null;\n  }\n\n  groupItemAdded(controlPoint: ControlPoint): void {\n    switch (controlPoint.type) {\n      case TimingControlPoint.TYPE:\n        this.timingPoints.add(controlPoint as TimingControlPoint);\n        break;\n      case EffectControlPoint.TYPE:\n        this.effectPoints.add(controlPoint as EffectControlPoint);\n        break;\n      case SampleControlPoint.TYPE:\n        this.samplePoints.add(controlPoint as SampleControlPoint);\n        break;\n      case DifficultyControlPoint.TYPE:\n        this.difficultyPoints.add(controlPoint as DifficultyControlPoint);\n        break;\n    }\n  }\n\n  groupItemRemoved(controlPoint: ControlPoint): void {\n    switch (controlPoint.type) {\n      case TimingControlPoint.TYPE:\n        this.timingPoints.remove(controlPoint as TimingControlPoint);\n        break;\n      case EffectControlPoint.TYPE:\n        this.effectPoints.remove(controlPoint as EffectControlPoint);\n        break;\n      case SampleControlPoint.TYPE:\n        this.samplePoints.remove(controlPoint as SampleControlPoint);\n        break;\n      case DifficultyControlPoint.TYPE:\n        this.difficultyPoints.remove(controlPoint as DifficultyControlPoint);\n        break;\n    }\n  }\n\n  timingPointAt(time: number): TimingControlPoint {\n    return this.binarySearchWithFallback(\n      this.timingPoints.list,\n      time,\n      this.timingPoints.length > 0 ? this.timingPoints.get(0) : TimingControlPoint.DEFAULT,\n    );\n  }\n\n  effectPointAt(time: number): EffectControlPoint {\n    return this.binarySearchWithFallback(this.effectPoints.list, time, EffectControlPoint.DEFAULT);\n  }\n\n  binarySearchWithFallback<T extends ControlPoint>(list: T[], time: number, fallback: T): T {\n    const obj = this.binarySearch(list, time);\n    return obj ?? fallback;\n  }\n\n  // Find the first element that has a time not less than the given time.\n  binarySearch<T extends ControlPoint>(list: T[], time: number): T | null {\n    if (list === null) throw new Error(\"Argument null\");\n    if (list.length === 0 || time < list[0].time) return null;\n\n    let lo = 0;\n    let hi = list.length;\n    // Find the first index that has a time greater than current one.\n    // The previous one will then be the answer.\n    while (lo < hi) {\n      const mid = lo + ((hi - lo) >> 1);\n      if (list[mid].time <= time) lo = mid + 1;\n      else hi = mid;\n    }\n    return list[lo - 1];\n  }\n\n  private checkAlreadyExisting(time: number, newPoint: ControlPoint): boolean {\n    let existing: ControlPoint | null = null;\n\n    switch (newPoint.type) {\n      case TimingControlPoint.TYPE:\n        existing = this.binarySearch(this.timingPoints.list, time);\n        break;\n      case EffectControlPoint.TYPE:\n        existing = this.effectPointAt(time);\n        break;\n      case SampleControlPoint.TYPE:\n        existing = this.binarySearch(this.samplePoints.list, time);\n        break;\n      case DifficultyControlPoint.TYPE:\n        existing = this.difficultyPointAt(time);\n        break;\n    }\n\n    // TODO: in osu!lazer it's written with newPoint?.isRedundant\n    return existing ? newPoint.isRedundant(existing) : false;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/ControlPoints/DifficultyControlPoint.ts",
    "content": "import { ControlPoint } from \"./ControlPoint\";\n\nexport class DifficultyControlPoint extends ControlPoint {\n  static DEFAULT: DifficultyControlPoint = new DifficultyControlPoint();\n  static TYPE = \"DifficultyControlPoint\";\n  speedMultiplier = 1;\n\n  get type(): string {\n    return DifficultyControlPoint.TYPE;\n  }\n\n  isRedundant(existing: ControlPoint): boolean {\n    return false;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/ControlPoints/EffectControlPoint.ts",
    "content": "import { ControlPoint } from \"./ControlPoint\";\n\nexport class EffectControlPoint extends ControlPoint {\n  static TYPE = \"EffectControlPoint\";\n  static DEFAULT = new EffectControlPoint();\n\n  kiaiMode = false;\n  omitFirstBarLine = true;\n\n  isRedundant(existing: ControlPoint): boolean {\n    if (this.omitFirstBarLine || existing.type !== EffectControlPoint.TYPE) return false;\n    const e = existing as EffectControlPoint;\n    return this.kiaiMode === e.kiaiMode && this.omitFirstBarLine === e.omitFirstBarLine;\n  }\n\n  get type(): string {\n    return EffectControlPoint.TYPE;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/ControlPoints/SampleControlPoint.ts",
    "content": "import { ControlPoint } from \"./ControlPoint\";\n\nexport class SampleControlPoint extends ControlPoint {\n  static TYPE = \"SampleControlPoint\";\n  static DEFAULT_BANK = \"normal\";\n  static DEFAULT = new SampleControlPoint();\n\n  sampleBank = SampleControlPoint.DEFAULT_BANK;\n  sampleVolume = 100;\n\n  isRedundant(existing: ControlPoint): boolean {\n    if (existing.type !== SampleControlPoint.TYPE) return false;\n    const e = existing as SampleControlPoint;\n    return this.sampleBank === e.sampleBank && this.sampleVolume === e.sampleVolume;\n  }\n\n  get type(): string {\n    return SampleControlPoint.TYPE;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/ControlPoints/TimingControlPoint.ts",
    "content": "import { ControlPoint } from \"./ControlPoint\";\nimport { TimeSignatures } from \"../TimeSignatures\";\n\nexport class TimingControlPoint extends ControlPoint {\n  static DEFAULT: TimingControlPoint;\n  static TYPE = \"TimingControlPoint\";\n\n  beatLength = 1000;\n  // TODO: Is this the default value?\n  timeSignature: TimeSignatures = TimeSignatures.SimpleQuadruple;\n\n  // The BPM at this control point\n  get BPM(): number {\n    return 60000 / this.beatLength;\n  }\n\n  get type(): string {\n    return TimingControlPoint.TYPE;\n  }\n\n  isRedundant(existing: ControlPoint): boolean {\n    return false;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/LegacyEffectFlag.ts",
    "content": "export enum LegacyEffectFlags {\n  None = 0,\n  Kiai = 1,\n  OmitFirstBarLine = 8,\n}\n"
  },
  {
    "path": "libs/osu/core/src/beatmap/TimeSignatures.ts",
    "content": "export enum TimeSignatures {\n  SimpleQuadruple = 4,\n  SimpleTriple = 3,\n}\n"
  },
  {
    "path": "libs/osu/core/src/blueprint/Blueprint.ts",
    "content": "import { ControlPointInfo } from \"../beatmap/ControlPoints/ControlPointInfo\";\nimport { HitObjectSettings } from \"./HitObjectSettings\";\nimport { BeatmapDifficulty } from \"../beatmap/BeatmapDifficulty\";\n\n/**\n * Blueprints define the default settings of the hitobjects.\n *\n * Beatmaps are instances of blueprints with additional modifications:\n * - Game mods, e.g. HardRock flips the map vertically\n * - Hit object stacking\n *\n * The reason that we have to explicitly distinguish between beatmaps and blueprint is due to clarity:\n * * Players don't play on blueprints, they play on beatmaps.\n * * Difficulty calculation is also not done on blueprints, but on beatmaps.\n * * ...\n */\n\nexport interface Blueprint {\n  blueprintInfo: BlueprintInfo;\n  defaultDifficulty: BeatmapDifficulty;\n  controlPointInfo: ControlPointInfo;\n  hitObjectSettings: HitObjectSettings[];\n}\n\nexport interface BlueprintInfo {\n  beatmapVersion: number;\n  onlineBeatmapId?: number;\n  metadata: BlueprintMetadata;\n  audioLeadIn: number; // 0\n  stackLeniency: number; // 0.7\n}\n\nexport interface BlueprintMetadata {\n  artist: string;\n  title: string;\n  titleUnicode: string;\n  artistUnicode: string;\n  source: string;\n  tags: string;\n  previewTime: number;\n  audioFile: string;\n  backgroundFile: string;\n  backgroundOffset: { x: number; y: number };\n}\n"
  },
  {
    "path": "libs/osu/core/src/blueprint/BlueprintParser.spec.ts",
    "content": "import { Position } from \"@osujs/math\";\nimport { parseBlueprint, parseOsuHitObjectSetting } from \"./BlueprintParser\";\nimport { HitCircleSettings, HitObjectSettingsType, SliderSettings, SpinnerSettings } from \"./HitObjectSettings\";\nimport { PathControlPoint } from \"../hitobjects/slider/PathControlPoint\";\nimport { PathType } from \"../hitobjects/slider/PathType\";\nimport { BeatmapDifficulty } from \"../beatmap/BeatmapDifficulty\";\n\nfunction mapToOffsets(controlPoints: PathControlPoint[]): Position[] {\n  return controlPoints.map((p) => p.offset);\n}\n\n/**\n * Unit testing the parseOsuHitObjectSetting\n */\ndescribe(\"parseOsuHitObjectSetting\", function () {\n  it(\"Simple HitCircle\", function () {\n    const settings = parseOsuHitObjectSetting(\"256,192,11000,21,2\") as HitCircleSettings;\n    expect(settings.type).toBe(\"HIT_CIRCLE\");\n    expect(settings.position.x).toBe(256);\n    expect(settings.position.y).toBe(192);\n    expect(settings.time).toBe(11000);\n    expect(settings.newCombo).toBe(true);\n    expect(settings.comboSkip).toBe(1);\n    // TODO: Sample\n  });\n\n  it(\"Slider with Bezier type\", function () {\n    const settings = parseOsuHitObjectSetting(\n      \"100,101,12600,6,1,B|200:200|250:200|250:200|300:150,2,310.123,2|1|2,0:0|0:0|0:2,0:0:0:0:\",\n    ) as SliderSettings;\n    expect(settings.type).toBe(\"SLIDER\");\n    expect(settings.position.x).toBe(100);\n    expect(settings.position.y).toBe(101);\n    expect(settings.time).toBe(12600);\n    expect(settings.newCombo).toBe(true);\n    expect(settings.comboSkip).toBe(0);\n    expect(mapToOffsets(settings.pathPoints)).toEqual([\n      { x: 0, y: 0 },\n      { x: 100, y: 99 },\n      { x: 150, y: 99 },\n      { x: 200, y: 49 },\n    ]);\n    expect(settings.pathPoints[0].type).toBe(PathType.Bezier);\n    // expect(settings.pathPoints[1].type).to.be.undefined;\n    // expect(settings.pathPoints[2].type).to.be.undefined; // Bezier ???\n    // expect(settings.pathPoints[3].type).to.be.undefined;\n    // TODO: Sample\n  });\n  it(\"Slider with Centripetal Catmull–Rom type\", function () {\n    const settings = parseOsuHitObjectSetting(\n      \"100,-101,12600,6,1,C|200:200|300:412,2,310.123,2|1|2,0:0|0:0|0:2,0:0:0:0:\",\n    ) as SliderSettings;\n    const expectedType: HitObjectSettingsType = \"SLIDER\";\n    expect(settings.type).toBe(expectedType);\n    expect(settings.position.x).toBe(100);\n    expect(settings.position.y).toBe(-101);\n    expect(settings.time).toBe(12600);\n    expect(settings.newCombo).toBe(true);\n    expect(settings.comboSkip).toBe(0);\n    // expect(settings.pathPoints).to.be.deep.equal([{x: 200, y: 200}, {x: 300, y: 412}]);\n    // TODO: Sample\n  });\n  it(\"Spinner\", function () {\n    // Spinner from https://osu.ppy.sh/beatmapsets/803535#osu/1686476\n    const settings = parseOsuHitObjectSetting(\"256,192,10892,12,0,21460,0:0:0:0:\");\n    expect(settings.type).toBe(\"SPINNER\");\n\n    const spinnerSettings = settings as SpinnerSettings;\n    expect(spinnerSettings.position).toStrictEqual({ x: 256, y: 192 });\n    expect(spinnerSettings.time).toBe(10892);\n    expect(spinnerSettings.duration).toBe(21460 - 10892);\n  });\n});\n\ndescribe(\"Parsing beatmap difficulty\", function () {\n  it(\"with missing approach rate\", function () {\n    // From \"Banned Forever\" https://osu.ppy.sh/beatmapsets/16349#osu/64266\n    const data = `\nosu file format v7\n[Difficulty]\nHPDrainRate:6\nCircleSize:5\nOverallDifficulty:8\nSliderMultiplier:1.6\nSliderTickRate:1\n`;\n    const { defaultDifficulty } = parseBlueprint(data, { sectionsToRead: [\"Difficulty\"] });\n    expect(defaultDifficulty).toEqual<BeatmapDifficulty>({\n      drainRate: 6,\n      circleSize: 5,\n      overallDifficulty: 8,\n      // It takes the OD value, source:\n      // https://github.com/ppy/osu/blob/b1fcb840a9ff4d866aac262ace7f54fa88b5e0ce/osu.Game/Beatmaps/BeatmapDifficulty.cs#L35\n      approachRate: 8,\n      sliderMultiplier: 1.6,\n      sliderTickRate: 1,\n    });\n  });\n  it(\"with missing OD and AR\", function () {\n    // OD is by default 5, so the AR should be 5 as well\n    const data = `\nosu file format v7\n[Difficulty]\nHPDrainRate:6\nCircleSize:5\n// OverallDifficulty:8\nSliderMultiplier:1.6\nSliderTickRate:1\n`;\n    const { defaultDifficulty } = parseBlueprint(data, { sectionsToRead: [\"Difficulty\"] });\n    expect(defaultDifficulty).toEqual<BeatmapDifficulty>({\n      drainRate: 6,\n      circleSize: 5,\n      overallDifficulty: 5,\n      // It takes the OD value, source:\n      // https://github.com/ppy/osu/blob/b1fcb840a9ff4d866aac262ace7f54fa88b5e0ce/osu.Game/Beatmaps/BeatmapDifficulty.cs#L35\n      approachRate: 5,\n      sliderMultiplier: 1.6,\n      sliderTickRate: 1,\n    });\n  });\n});\n\ndescribe(\"Parsing a well formatted .osu file: ZUTOMAYO - Kan Saete Kuyashiiwa (Nathan) [geragera].osu\", function () {\n  const data = `osu file format v14\n[General]\nAudioFilename: audio.mp3\nAudioLeadIn: 0\nPreviewTime: 69709\nCountdown: 0\nSampleSet: Soft\nStackLeniency: 0.7\nMode: 0\nLetterboxInBreaks: 0\nWidescreenStoryboard: 0\n\n[Editor]\nDistanceSpacing: 0.3\nBeatDivisor: 4\nGridSize: 8\nTimelineZoom: 1.9\n\n[Metadata]\nTitle:Kan Saete Kuyashiiwa\nTitleUnicode:勘冴えて悔しいわ\nArtist:ZUTOMAYO\nArtistUnicode:ずっと真夜中でいいのに。\nCreator:Nathan\nVersion:geragera\nSource:\nTags:zutto mayonaka de ii no ni. acane sukinathan scubdomino luscent 404_aimnotfound\nBeatmapID:2096523\nBeatmapSetID:1001507\n\n[Difficulty]\nHPDrainRate:5\nCircleSize:4\nOverallDifficulty:9\nApproachRate:9.6\nSliderMultiplier:1.8\nSliderTickRate:1\n\n[Events]\n//Background and Video events\n0,0,\"thistook3yearstoresize.png\",0,0\n//Break Periods\n2,98517,103577\n2,181510,183385\n//Storyboard Layer 0 (Background)\n//Storyboard Layer 1 (Fail)\n//Storyboard Layer 2 (Pass)\n//Storyboard Layer 3 (Foreground)\n//Storyboard Layer 4 (Overlay)\n//Storyboard Sound Samples\n\n[TimingPoints]\n309,400,4,2,1,80,1,0\n509,400,4,2,1,80,1,0\n909,-76.9230769230769,4,2,1,80,0,0\n// Rest are omitted\n\n[Colours]\nCombo1 : 255,94,55\nCombo2 : 173,255,95\nCombo3 : 17,254,176\nCombo4 : 255,88,100\n\n[HitObjects]\n340,221,309,6,0,P|337:205|333:191,3,22.5,2|2|2|2,0:0|0:0|0:0|0:0,0:0:0:0:\n342,239,509,5,6,3:2:0:0:\n// Rest are omitted\n`;\n  const bp = parseBlueprint(data);\n  it(\"should parse the meta data correctly\", function () {\n    expect(bp.blueprintInfo.metadata).toEqual(\n      expect.objectContaining({\n        artist: \"ZUTOMAYO\",\n        artistUnicode: \"ずっと真夜中でいいのに。\",\n        title: \"Kan Saete Kuyashiiwa\",\n        titleUnicode: \"勘冴えて悔しいわ\",\n        tags: \"zutto mayonaka de ii no ni. acane sukinathan scubdomino luscent 404_aimnotfound\",\n        audioFile: \"audio.mp3\",\n        // TODO: previewTime: 69709\n      }),\n    );\n  });\n  it(\"should parse the beatmap version correctly\", function () {\n    expect(bp.blueprintInfo.beatmapVersion).toBe(14);\n  });\n  it(\"should read some hit objects\", function () {\n    expect(bp.hitObjectSettings.length).toEqual(2);\n  });\n  it(\"should parse blueprint default difficulty correctly\", function () {\n    expect(bp.defaultDifficulty).toEqual({\n      drainRate: 5,\n      circleSize: 4,\n      overallDifficulty: 9,\n      approachRate: 9.6,\n      sliderMultiplier: 1.8,\n      sliderTickRate: 1,\n    });\n  });\n\n  it(\"should have the timing control points ordered non-decreasingly by start time\", function () {\n    const { timingPoints } = bp.controlPointInfo;\n    for (let i = 0; i + 1 < timingPoints.length; i++) {\n      const a = timingPoints.get(i);\n      const b = timingPoints.get(i + 1);\n      expect(a.time <= b.time).toBeTruthy();\n      //  `Not ordered properly at i=${i}: ${a.time} <= ${b.time}`\n    }\n  });\n});\n"
  },
  {
    "path": "libs/osu/core/src/blueprint/BlueprintParser.ts",
    "content": "import { Blueprint, BlueprintInfo } from \"./Blueprint\";\nimport { clamp, floatEqual, Position, Vec2 } from \"@osujs/math\";\nimport { PathControlPoint } from \"../hitobjects/slider/PathControlPoint\";\nimport { PathType } from \"../hitobjects/slider/PathType\";\nimport { TimeSignatures } from \"../beatmap/TimeSignatures\";\nimport { LegacyEffectFlags } from \"../beatmap/LegacyEffectFlag\";\nimport { ControlPoint } from \"../beatmap/ControlPoints/ControlPoint\";\nimport { TimingControlPoint } from \"../beatmap/ControlPoints/TimingControlPoint\";\nimport { DifficultyControlPoint } from \"../beatmap/ControlPoints/DifficultyControlPoint\";\nimport { EffectControlPoint } from \"../beatmap/ControlPoints/EffectControlPoint\";\nimport { SampleControlPoint } from \"../beatmap/ControlPoints/SampleControlPoint\";\nimport { HitSampleInfo } from \"../audio/HitSampleInfo\";\nimport { LegacySampleBank } from \"../audio/LegacySampleBank\";\nimport { HitCircleSettings, HitObjectSettings, SliderSettings, SpinnerSettings } from \"./HitObjectSettings\";\nimport { BeatmapDifficulty } from \"../beatmap/BeatmapDifficulty\";\nimport { Optional } from \"utility-types\";\nimport { ControlPointInfo } from \"../beatmap/ControlPoints/ControlPointInfo\";\n\nconst SECTION_REGEX = /^\\s*\\[(.+?)]\\s*$/;\nconst DEFAULT_LEGACY_TICK_OFFSET = 36;\n\n/**\n *  Will make sure that the comment at the end of line is removed\n *  Given \"0, 1, 2 // <- Test\"\n *  Returns \"0, 1, 2\"\n */\nfunction stripComments(line: string): string {\n  const index = line.indexOf(\"//\");\n  if (index >= 0) {\n    return line.substr(0, index);\n  } else {\n    return line;\n  }\n}\n\nenum LegacyHitObjectType {\n  Circle = 1,\n  Slider = 1 << 1,\n  NewCombo = 1 << 2,\n  Spinner = 1 << 3,\n  ComboSkip = (1 << 4) | (1 << 5) | (1 << 6),\n  Hold = 1 << 7,\n}\n\nenum LegacyHitSoundType {\n  None = 0,\n  Normal = 1,\n  Whistle = 2,\n  Finish = 4,\n  Clap = 8,\n}\n\nexport class LegacyHitSampleInfo extends HitSampleInfo {\n  isLayered: boolean;\n  customSampleBank: number;\n\n  constructor(name: string, bank: string | null = null, volume = 0, customSampleBank = 0, isLayered = false) {\n    super(name, bank, customSampleBank >= 2 ? customSampleBank.toString() : null, volume);\n    this.isLayered = isLayered;\n    this.customSampleBank = customSampleBank;\n  }\n}\n\nfunction hasFlag(bitmask: number, flag: number): boolean {\n  return (bitmask & flag) !== 0;\n}\n\nfunction splitKeyVal(line: string, separator = \":\"): [string, string] {\n  const split = line.split(separator, 2);\n  return [split[0].trim(), split[1]?.trim() ?? \"\"];\n}\n\nfunction convertPathType(value: string): PathType {\n  switch (value[0]) {\n    // TODO: ???\n    default:\n    case \"C\":\n      return PathType.Catmull;\n    case \"B\":\n      return PathType.Bezier;\n    case \"L\":\n      return PathType.Linear;\n    case \"P\":\n      return PathType.PerfectCurve;\n  }\n}\n\n// ???\nclass LegacyDifficultyControlPoint extends DifficultyControlPoint {\n  bpmMultiplier: number;\n\n  constructor(beatLength: number) {\n    super();\n    // Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to\n    // occur on doubles via some .NET black magic (possibly inlining?).\n    this.bpmMultiplier = beatLength < 0 ? clamp(-beatLength, 10, 10000) / 100.0 : 1;\n  }\n}\n\n// https://github.com/ppy/osu/blob/82848a7d70e8bb1dbd7bed2b4a178dfe2ce94bcc/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs#L391\ntype SampleBankInfo = {\n  fileName: string;\n  normal: string;\n  add: string;\n  volume: number;\n  customSampleBank: number;\n};\n\nfunction convertPathString(pointString: string, offset: Position): PathControlPoint[] {\n  const pointSplit = pointString.split(\"|\");\n  const controlPoints: PathControlPoint[] = [];\n  let startIndex = 0;\n  let endIndex = 0;\n  let first = true;\n\n  while (++endIndex < pointSplit.length) {\n    // Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of\n    // length 1).\n    if (pointSplit[endIndex].length > 1) continue;\n    // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the\n    // start of the next segment. The start of the next segment is the index after the type descriptor.\n    const endPoint = endIndex < pointSplit.length - 1 ? pointSplit[endIndex + 1] : null;\n\n    const points = convertPoints(pointSplit.slice(startIndex, endIndex), endPoint, first, offset);\n    controlPoints.push(...points);\n    startIndex = endIndex;\n    first = false;\n  }\n  if (endIndex > startIndex) {\n    controlPoints.push(...convertPoints(pointSplit.slice(startIndex, endIndex), null, first, offset));\n  }\n  return controlPoints;\n}\n\n// reads the relative position from the given `startPos`\nfunction readPoint(value: string, startPos: Position, points: PathControlPoint[], index: number): void {\n  const [x, y] = value.split(\":\").map(parseFloat);\n  const position = Vec2.sub({ x, y }, startPos);\n  points[index] = { offset: position };\n}\n\nfunction isLinear(p: Position[]): boolean {\n  return floatEqual(0, (p[1].y - p[0].y) * (p[2].x - p[0].x) - (p[1].x - p[0].x) * (p[2].y - p[0].y));\n}\n\nfunction convertPoints(\n  points: string[],\n  endPoint: string | null,\n  first: boolean,\n  offset: Position,\n): PathControlPoint[] {\n  let type: PathType = convertPathType(points[0]);\n  const readOffset = first ? 1 : 0;\n  const readablePoints = points.length - 1;\n  const endPointLength = endPoint !== null ? 1 : 0;\n\n  const vertices: PathControlPoint[] = new Array(readOffset + readablePoints + endPointLength);\n\n  // Fill any non-read points\n  for (let i = 0; i < readOffset; i++) vertices[i] = { offset: Vec2.Zero };\n  // Parse into control points.\n  for (let i = 1; i < points.length; i++) readPoint(points[i], offset, vertices, readOffset + i - 1);\n  if (endPoint !== null) readPoint(endPoint, offset, vertices, vertices.length - 1);\n\n  if (type === PathType.PerfectCurve) {\n    if (vertices.length !== 3) type = PathType.Bezier;\n    else if (isLinear(vertices.map((v) => v.offset))) type = PathType.Linear;\n  }\n\n  vertices[0].type = type;\n  let startIndex = 0;\n  let endIndex = 0;\n\n  const result: PathControlPoint[] = [];\n\n  // this is just some logic to not have duplicated positions at the end\n  while (++endIndex < vertices.length - endPointLength) {\n    if (!Vec2.equal(vertices[endIndex].offset, vertices[endIndex - 1].offset)) {\n      continue;\n    }\n\n    // The last control point of each segment is not allowed to start a new implicit segment.\n    if (endIndex === vertices.length - endPointLength - 1) {\n      continue;\n    }\n\n    vertices[endIndex - 1].type = type;\n    result.push(...vertices.slice(startIndex, endIndex));\n\n    startIndex = endIndex + 1;\n  }\n  if (endIndex > startIndex) {\n    result.push(...vertices.slice(startIndex, endIndex));\n  }\n  return result;\n}\n\nexport function parseOsuHitObjectSetting(line: string): HitCircleSettings | SliderSettings | SpinnerSettings {\n  const split = line.split(\",\");\n\n  // TODO: This has MAX_COORDINATE_VALUE for sanity check\n  const position = { x: parseFloat(split[0]), y: parseFloat(split[1]) };\n  // TODO: This has +offset (24ms) for beatmapVersion <= 4 (include in BeatmapBuilder)\n  const offset = 0; //\n  const time = parseFloat(split[2]) + offset;\n\n  const _type = parseInt(split[3]); // also has combo information\n  const comboSkip = (_type & LegacyHitObjectType.ComboSkip) >> 4;\n  const newCombo: boolean = hasFlag(_type, LegacyHitObjectType.NewCombo);\n\n  const typeBitmask = _type & ~LegacyHitObjectType.ComboSkip & ~LegacyHitObjectType.NewCombo;\n\n  // TODO: samples\n  const soundType: number = parseInt(split[4]);\n  const bankInfo: SampleBankInfo = {\n    add: \"\",\n    customSampleBank: 0,\n    fileName: \"\",\n    normal: \"\",\n    volume: 0,\n  };\n\n  if (hasFlag(typeBitmask, LegacyHitObjectType.Circle)) {\n    // TODO: CustomSampleBanks not supported yet\n    // if (split.length > 5) readCustomSampleBanks(split[5], bankInfo);\n    return {\n      type: \"HIT_CIRCLE\",\n      time,\n      position,\n      newCombo,\n      comboSkip,\n    };\n  }\n  if (hasFlag(typeBitmask, LegacyHitObjectType.Slider)) {\n    let length: number | undefined;\n    const slides = parseInt(split[6]);\n    if (slides > 9000) throw new Error(\"Slides count is way too high\");\n\n    const repeatCount = Math.max(0, slides - 1);\n\n    const pathPoints = convertPathString(split[5], position);\n\n    if (split.length > 7) {\n      length = Math.max(0, parseFloat(split[7]));\n      if (length === 0) length = undefined;\n    }\n\n    return {\n      type: \"SLIDER\",\n      time,\n      position,\n      repeatCount,\n      comboSkip,\n      newCombo,\n      pathPoints,\n      length,\n      legacyLastTickOffset: DEFAULT_LEGACY_TICK_OFFSET,\n      tickDistanceMultiplier: 1,\n    };\n  }\n  if (hasFlag(typeBitmask, LegacyHitObjectType.Spinner)) {\n    const duration = Math.max(0, parseFloat(split[5]) + offset - time);\n    return {\n      type: \"SPINNER\",\n      comboSkip,\n      newCombo,\n      time,\n      position,\n      duration,\n    };\n  }\n  throw Error(\"Unknown type\");\n}\n\ntype BlueprintDifficulty = Optional<BeatmapDifficulty, \"approachRate\">;\n\nconst defaultBlueprintInfo = (): BlueprintInfo => ({\n  audioLeadIn: 0,\n  beatmapVersion: 0,\n  stackLeniency: 0.7,\n  onlineBeatmapId: undefined,\n  metadata: {\n    artist: \"\",\n    title: \"\",\n    titleUnicode: \"\",\n    audioFile: \"\",\n    artistUnicode: \"\",\n    source: \"\",\n    tags: \"\",\n    previewTime: 0,\n    backgroundFile: \"\",\n    backgroundOffset: { x: 0, y: 0 },\n  },\n});\n\nconst defaultBlueprintDifficulty = (): BlueprintDifficulty => ({\n  circleSize: 5,\n  drainRate: 5,\n  overallDifficulty: 5,\n  // approachRate omitted because it depends on OD\n  sliderMultiplier: 1,\n  sliderTickRate: 1,\n});\n\nclass BlueprintParser {\n  static LATEST_VERSION = 14;\n\n  data: string;\n  currentSection: BlueprintSection | null;\n\n  offset: number;\n\n  // Disable for testing purposes\n  applyOffsets = true;\n\n  formatVersion: number;\n\n  defaultSampleVolume = 100;\n  blueprintInfo: BlueprintInfo;\n  blueprintDifficulty: BlueprintDifficulty;\n  hitObjectSettings: HitObjectSettings[] = [];\n  controlPointInfo: ControlPointInfo = new ControlPointInfo();\n\n  defaultSampleBank: LegacySampleBank = LegacySampleBank.None;\n\n  readonly sectionsToRead: readonly BlueprintSection[];\n  readonly sectionsFinishedReading: string[];\n\n  constructor(data: string, options: BlueprintParseOptions = defaultOptions) {\n    this.data = data;\n    // this.blueprint = new Blueprint();\n    this.currentSection = null;\n    this.formatVersion = options.formatVersion;\n    this.sectionsToRead = options.sectionsToRead;\n    this.blueprintInfo = defaultBlueprintInfo();\n    this.blueprintDifficulty = defaultBlueprintDifficulty();\n    this.sectionsFinishedReading = [];\n\n    // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)\n    this.offset = this.formatVersion <= 4 ? 24 : 0;\n    // this.hitObjectParser = new OsuHitObjectParser(this.offset, this.formatVersion);\n  }\n\n  isFinishedReading() {\n    return this.sectionsToRead <= this.sectionsFinishedReading;\n  }\n\n  parseLine(line: string): void {\n    const strippedLine = stripComments(line);\n    // strippedLine can be empty\n    if (!strippedLine) return;\n    // Parse the file format\n    if (!this.currentSection && strippedLine.includes(\"osu file format v\")) {\n      this.blueprintInfo.beatmapVersion = parseInt(strippedLine.split(\"osu file format v\")[1], 10);\n      return;\n    }\n    if (SECTION_REGEX.test(strippedLine)) {\n      // We only add sections we want to read to the list\n      if (this.currentSection !== null && this.sectionsToRead.includes(this.currentSection)) {\n        this.sectionsFinishedReading.push(this.currentSection);\n      }\n      this.currentSection = (SECTION_REGEX.exec(strippedLine) as RegExpExecArray)[1] as BlueprintSection;\n      // It will stop when we are done with reading all required sections\n      return;\n    }\n\n    // We skip reading sections we don't want to read for optimization\n    if (this.currentSection === null || this.sectionsToRead.indexOf(this.currentSection) === -1) {\n      return;\n    }\n\n    switch (this.currentSection) {\n      case \"General\":\n        this.handleGeneral(strippedLine);\n        break;\n      case \"Metadata\":\n        this.handleMetadata(strippedLine);\n        break;\n      case \"Difficulty\":\n        this.handleDifficulty(strippedLine);\n        break;\n      case \"HitObjects\":\n        this.handleHitObjects(strippedLine);\n        break;\n      case \"TimingPoints\":\n        this.handleTimingPoints(strippedLine);\n        break;\n      // Below are low priority sections\n      case \"Events\":\n        this.handleEvents(strippedLine);\n        break;\n      case \"Editor\":\n        this.handleEditor(strippedLine);\n        break;\n      case \"Colours\":\n        break;\n    }\n  }\n\n  handleEvents(line: string) {\n    const [eventType, _startTime, ...eventParams] = line.split(\",\");\n    switch (eventType) {\n      case \"0\": {\n        const [filename, xOffset, yOffset] = eventParams;\n        // The quotes can optionally be given ...\n        this.blueprintInfo.metadata.backgroundFile = filename.replace(/\"/g, \"\");\n        this.blueprintInfo.metadata.backgroundOffset = {\n          // In case they weren't provided: 0,0 should be used according to docs.\n          x: parseInt(xOffset ?? \"0\"),\n          y: parseInt(yOffset ?? \"0\"),\n        };\n      }\n      // Videos and Storyboard ignored for first...\n    }\n  }\n\n  handleGeneral(line: string): void {\n    const [key, value] = splitKeyVal(line);\n    const blueprintInfo = this.blueprintInfo;\n    const metadata = blueprintInfo.metadata;\n    switch (key) {\n      case \"AudioFilename\":\n        metadata.audioFile = value; // TODO: toStandardisedPath()\n        break;\n      case \"AudioLeadIn\":\n        blueprintInfo.audioLeadIn = parseInt(value);\n        break;\n      case \"PreviewTime\":\n        break;\n      case \"Countdown\":\n        break;\n      case \"SampleSet\":\n        this.defaultSampleBank = parseInt(value); // hopefully it is one of those 4\n        break;\n      case \"SampleVolume\":\n        break;\n      case \"StackLeniency\":\n        blueprintInfo.stackLeniency = Math.fround(parseFloat(value));\n        break;\n      case \"Mode\":\n        break;\n      case \"LetterboxInBreaks\":\n        break;\n      case \"SpecialStyle\":\n        break;\n      case \"WidescreenStoryboard\":\n        break;\n      case \"EpilepsyWarning\":\n        break;\n    }\n  }\n\n  handleEditor(line: string): void {\n    const [key, value] = splitKeyVal(line);\n    switch (key) {\n      case \"Bookmarks\":\n        break;\n      case \"DistanceSpacing\":\n        break;\n      case \"BeatDivisor\":\n        break;\n      case \"GridSize\":\n        break;\n      case \"TimelineZoom\":\n        break;\n    }\n  }\n\n  handleMetadata(line: string): void {\n    const [key, value] = splitKeyVal(line);\n    const blueprintInfo = this.blueprintInfo;\n    const metaData = blueprintInfo.metadata;\n    switch (key) {\n      case \"Title\":\n        metaData.title = value;\n        break;\n      case \"TitleUnicode\":\n        metaData.titleUnicode = value;\n        break;\n      case \"Artist\":\n        metaData.artist = value;\n        break;\n      case \"ArtistUnicode\":\n        metaData.artistUnicode = value;\n        break;\n      case \"Creator\":\n        // metaData.authorString = value;\n        break;\n      case \"Version\":\n        // beatmapInfo.beatmapVersion = value;\n        break;\n      case \"Source\":\n        break;\n      case \"Tags\":\n        metaData.tags = value;\n        break;\n      case \"BeatmapId\":\n        break;\n      case \"BeatmapSetID\":\n        break;\n    }\n  }\n\n  handleDifficulty(line: string): void {\n    const [key, value] = splitKeyVal(line);\n    const difficulty = this.blueprintDifficulty;\n    switch (key) {\n      case \"HPDrainRate\":\n        difficulty.drainRate = parseFloat(value);\n        break;\n      case \"CircleSize\":\n        difficulty.circleSize = parseFloat(value);\n        break;\n      case \"OverallDifficulty\":\n        difficulty.overallDifficulty = parseFloat(value);\n        break;\n      case \"ApproachRate\":\n        difficulty.approachRate = parseFloat(value);\n        break;\n      case \"SliderMultiplier\":\n        difficulty.sliderMultiplier = parseFloat(value);\n        break;\n      case \"SliderTickRate\":\n        difficulty.sliderTickRate = parseFloat(value);\n        break;\n    }\n  }\n\n  handleHitObjects(line: string): void {\n    const obj = parseOsuHitObjectSetting(line);\n    if (obj) {\n      this.hitObjectSettings.push(obj);\n    }\n  }\n\n  handleTimingPoints(line: string): void {\n    const split: string[] = line.split(\",\");\n\n    const time = this.getOffsetTime(parseFloat(split[0].trim()));\n    const beatLength = parseFloat(split[1].trim());\n    const speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;\n\n    let timeSignature = TimeSignatures.SimpleQuadruple;\n    if (split.length >= 3)\n      timeSignature = split[2][0] === \"0\" ? TimeSignatures.SimpleQuadruple : (parseInt(split[2]) as TimeSignatures);\n\n    // TODO: sampleSet default\n    let sampleSet: LegacySampleBank = this.defaultSampleBank;\n    if (split.length >= 4) sampleSet = parseInt(split[3]);\n\n    let customSampleBank = 0;\n    if (split.length >= 5) customSampleBank = parseInt(split[4]);\n\n    let sampleVolume = this.defaultSampleVolume;\n    if (split.length >= 6) sampleVolume = parseInt(split[5]);\n\n    let timingChange = true;\n    if (split.length >= 7) timingChange = split[6][0] === \"1\";\n\n    let kiaiMode = false;\n    let omitFirstBarSignature = false;\n    if (split.length >= 8) {\n      const effectFlags = parseInt(split[7]);\n      kiaiMode = hasFlag(effectFlags, LegacyEffectFlags.Kiai);\n      omitFirstBarSignature = hasFlag(effectFlags, LegacyEffectFlags.OmitFirstBarLine);\n    }\n\n    // This will receive the string value from the enum\n    let stringSampleSet = LegacySampleBank[sampleSet].toLowerCase();\n    if (stringSampleSet === \"none\") stringSampleSet = \"normal\";\n\n    if (timingChange) {\n      const controlPoint = this.createTimingControlPoint();\n      if (Number.isNaN(beatLength)) throw Error(\"NaN\");\n      else controlPoint.beatLength = clamp(beatLength, MIN_BEAT_LENGTH, MAX_BEAT_LENGTH);\n      controlPoint.timeSignature = timeSignature;\n      this.addControlPoint(time, controlPoint, true);\n    }\n\n    {\n      const p = new LegacyDifficultyControlPoint(beatLength);\n      p.speedMultiplier = bindableNumberNew(speedMultiplier, { min: 0.1, max: 10, precision: 0.01 });\n      this.addControlPoint(time, p, timingChange);\n    }\n    {\n      const p = new EffectControlPoint();\n      p.kiaiMode = kiaiMode;\n      p.omitFirstBarLine = omitFirstBarSignature;\n      this.addControlPoint(time, p, timingChange);\n    }\n    {\n      const p = new SampleControlPoint();\n      p.sampleBank = stringSampleSet;\n      p.sampleVolume = sampleVolume;\n      // TODO:  Need LegacySampleControlPoint, but this is something we support later on\n      // p.customSampleBank = customSampleBank;\n      this.addControlPoint(time, p, timingChange);\n    }\n  }\n\n  pendingControlPoints: ControlPoint[] = [];\n  pendingControlPointTypes: { [key: string]: boolean } = {};\n  pendingControlPointsTime = 0;\n\n  createTimingControlPoint(): TimingControlPoint {\n    return new TimingControlPoint();\n  }\n\n  addControlPoint(time: number, point: ControlPoint, timingChange: boolean): void {\n    if (!floatEqual(time, this.pendingControlPointsTime)) {\n      this.flushPendingPoints();\n    }\n    if (timingChange) {\n      this.pendingControlPoints.splice(0, 0, point);\n    } else {\n      this.pendingControlPoints.push(point);\n    }\n    this.pendingControlPointsTime = time;\n  }\n\n  flushPendingPoints(): void {\n    // Changes from non-timing-points are added to the end of the list (see addControlPoint()) and should override any\n    // changes from timing-points (added to the start of the list).\n    for (let i = this.pendingControlPoints.length - 1; i >= 0; i--) {\n      const type: string = this.pendingControlPoints[i].type;\n      if (this.pendingControlPointTypes[type]) continue;\n      this.pendingControlPointTypes[type] = true;\n      this.controlPointInfo.add(this.pendingControlPointsTime, this.pendingControlPoints[i]);\n    }\n    this.pendingControlPoints = [];\n    this.pendingControlPointTypes = {};\n  }\n\n  getOffsetTime(time: number): number {\n    return time + (this.applyOffsets ? this.offset : 0);\n  }\n\n  parse(): Blueprint {\n    if (!this.data) throw new Error(\"No data given\");\n    const lines = this.data.split(\"\\n\").map((v) => v.trim());\n    for (const line of lines) {\n      try {\n        this.parseLine(line);\n      } catch (err) {\n        console.error(`Failed to parse line ${line} due to: `, err);\n      }\n      if (this.isFinishedReading()) {\n        break;\n      }\n    }\n    this.flushPendingPoints();\n    return {\n      blueprintInfo: this.blueprintInfo,\n      defaultDifficulty: {\n        ...this.blueprintDifficulty,\n        // Reasoning:\n        // https://github.com/ppy/osu/blob/b1fcb840a9ff4d866aac262ace7f54fa88b5e0ce/osu.Game/Beatmaps/BeatmapDifficulty.cs#L35\n        approachRate: this.blueprintDifficulty.approachRate ?? this.blueprintDifficulty.overallDifficulty,\n      },\n      hitObjectSettings: this.hitObjectSettings,\n      controlPointInfo: this.controlPointInfo,\n    };\n  }\n}\n\nconst MIN_BEAT_LENGTH = 6;\nconst MAX_BEAT_LENGTH = 60000;\nconst DEFAULT_BEAT_LENGTH = 1000;\n\n// So precision is only used when not initializing?\n// The BindableNumber has a precision value but is not used when initialized\nfunction bindableNumberNew(val: number, { min, max, precision }: { min: number, max: number, precision: number }) {\n  return clamp(val, min, max);\n  // return Math.round(val / precision) * precision;\n}\n\nexport const BlueprintSections = [\n  \"General\",\n  \"Metadata\",\n  \"Difficulty\",\n  \"HitObjects\",\n  \"TimingPoints\",\n  \"Events\",\n  \"Editor\",\n  \"Colours\",\n] as const;\nexport type BlueprintSection = typeof BlueprintSections[number];\n\ninterface BlueprintParseOptions {\n  formatVersion: number;\n  sectionsToRead: readonly BlueprintSection[];\n}\n\nconst defaultOptions: BlueprintParseOptions = {\n  // TODO: Format version should actually be parsed\n  formatVersion: BlueprintParser.LATEST_VERSION,\n  sectionsToRead: BlueprintSections,\n};\n\n/**\n * Parses the blueprint that is given in the legacy `.osu` format.\n * @param data the .osu file string\n * @param {BlueprintParseOptions} options config options\n * @param {BlueprintParseOptions} options.sectionsToRead list of sections that should be read\n */\nexport function parseBlueprint(data: string, options?: Partial<BlueprintParseOptions>) {\n  const allOptions = Object.assign({ ...defaultOptions }, options);\n  const parser = new BlueprintParser(data, allOptions);\n  return parser.parse();\n}\n"
  },
  {
    "path": "libs/osu/core/src/blueprint/HitObjectSettings.ts",
    "content": "// https://osu.ppy.sh/wiki/en/osu%21_File_Formats/Osu_%28file_format%29#type\nimport { Position } from \"@osujs/math\";\nimport { PathControlPoint } from \"../hitobjects/slider/PathControlPoint\";\n\nexport type HitObjectSettingsType = \"HIT_CIRCLE\" | \"SLIDER\" | \"SPINNER\" | \"MANIA_HOLD\";\n\n// In the future (osu!lazer) there might be other settings such as custom circle size, custom AR, ...\nexport interface HitObjectSettings {\n  type: HitObjectSettingsType;\n  position: Position;\n  time: number;\n\n  // New combo or not\n  newCombo: boolean;\n  // An integer specifying how many combo colours to skip, if this object starts a new combo.\n  comboSkip: number;\n}\n\n// Not really sure if redundant\nexport interface HitCircleSettings extends HitObjectSettings {\n  type: \"HIT_CIRCLE\";\n}\n\nexport interface SliderSettings extends HitObjectSettings {\n  type: \"SLIDER\";\n\n  repeatCount: number;\n  // pathPoints consists of multiple segments while segment has a path type.\n  // e.g. `[Circle[p1, p2, p3], Bezier[p4, p5, p6, p7], Catmull[p8, p9, p10]]`.\n  // The type of the segment is determined by its first point, so in the above's it would be in p1, p4 and p8.\n  pathPoints: PathControlPoint[];\n  length?: number;\n\n  // 36\n  legacyLastTickOffset: number;\n\n  // In osu!lazer = 1. While parsing it might differ depending on osuBeatmapVersion\n  tickDistanceMultiplier: 1;\n}\n\nexport interface SpinnerSettings extends HitObjectSettings {\n  type: \"SPINNER\";\n  duration: number;\n  // x and y do not affect spinners. They default to the centre of the playfield, 256,192.\n}\n"
  },
  {
    "path": "libs/osu/core/src/gameplay/GameState.ts",
    "content": "import { Position, Vec2 } from \"@osujs/math\";\nimport { MainHitObjectVerdict } from \"./Verdicts\";\n\nexport const NOT_PRESSING = +727727727;\n\nexport type PressingSinceTimings = number[];\n\nconst HitCircleMissReasons = [\n  // When the time has expired and the circle got force killed.\n  \"TIME_EXPIRED\",\n  // There is no HIT_TOO_LATE because TIME_EXPIRED would occur earlier.\n  \"HIT_TOO_EARLY\",\n  // This is only possible in osu!lazer where clicking a later circle can cause this circle to be force missed.\n  \"FORCE_MISS_NOTELOCK\",\n  // If the user had time to press the hitCircle until time 300, but the slider is so short that it ends at 200,\n  // then the user actually has a reduced hit window for hitting it.\n  \"SLIDER_FINISHED_FASTER\",\n] as const;\n\nexport type HitCircleMissReason = typeof HitCircleMissReasons[number];\n\nexport type HitCircleVerdict = {\n  // Is the only that has a judgement time not being equal the hit object hitTime/startTime/endTime.\n  judgementTime: number;\n} & ({ type: \"GREAT\" | \"MEH\" | \"OK\" } | { type: \"MISS\"; missReason: HitCircleMissReason });\n\nexport type CheckPointState = {\n  hit: boolean;\n};\n\nexport type SliderBodyState = {\n  isTracking: boolean;\n};\n\nexport type SpinnerState = {\n  // Spinning 180 clock wise and 180 counter clock wise will give 360 degrees spun\n  // This is not rate adjusted\n  // The reason osu! lazer has a rate adjusted one is that the game clock play rate can change throughout time\n  totalSpunAngle: number;\n\n  // The current angle of the spinner hand\n  currentAngle: number;\n\n  // In other words:\n  // totalSpunAngle = sigma_i (abs(currentAngle[i] - currentAngle[i - 1])) with currentAngle[0] = 0\n};\n\nexport interface GameState {\n  // currentTime might be not really needed, but serves as an \"id\"\n  currentTime: number;\n  cursorPosition: Position;\n\n  // Keeps track of where we are in the events list (as an optimization).\n  eventIndex: number;\n\n  // The verdicts are a summary of how well these objects were played\n  hitCircleVerdict: Record<string, HitCircleVerdict>;\n  checkPointVerdict: Record<string, CheckPointState>;\n  sliderVerdict: Record<string, MainHitObjectVerdict>;\n  // spinnerVerdict ..\n\n  spinnerState: Map<string, SpinnerState>;\n  sliderBodyState: Map<string, SliderBodyState>;\n\n  clickWasUseful: boolean;\n\n  // Stores the ids of the objects that have been judged in the order of judgement.\n  // This can be used to easily derive the combo,maxCombo,accuracy,number of 300/100/50/misses, score\n  // This is only useful for knowing the order\n  judgedObjects: Array<string>;\n\n  // Rest are used for optimizations\n  latestHitObjectIndex: number;\n  aliveHitCircleIds: Set<string>;\n  aliveSliderIds: Set<string>;\n  aliveSpinnerIds: Set<string>;\n\n  pressingSince: PressingSinceTimings;\n}\n\nexport const defaultGameState = (): GameState => ({\n  eventIndex: 0,\n  currentTime: 0,\n  cursorPosition: Vec2.Zero,\n  hitCircleVerdict: {},\n  sliderBodyState: new Map<string, SliderBodyState>(),\n  checkPointVerdict: {},\n  spinnerState: new Map<string, SpinnerState>(),\n  sliderVerdict: {},\n\n  clickWasUseful: false,\n  // Rest are used for optimizations\n  latestHitObjectIndex: 0 as number,\n  aliveHitCircleIds: new Set<string>(),\n  aliveSliderIds: new Set<string>(),\n  aliveSpinnerIds: new Set<string>(),\n  // Also used as an optimization\n  judgedObjects: [],\n  pressingSince: [NOT_PRESSING, NOT_PRESSING],\n});\n\nexport function cloneGameState(replayState: GameState): GameState {\n  const {\n    aliveHitCircleIds,\n    aliveSliderIds,\n    aliveSpinnerIds,\n    spinnerState,\n    sliderBodyState,\n    checkPointVerdict,\n    hitCircleVerdict,\n    sliderVerdict,\n    eventIndex,\n    clickWasUseful,\n    currentTime,\n    cursorPosition,\n    latestHitObjectIndex,\n    pressingSince,\n    judgedObjects,\n  } = replayState;\n  return {\n    eventIndex: eventIndex,\n    aliveHitCircleIds: new Set<string>(aliveHitCircleIds),\n    aliveSliderIds: new Set<string>(aliveSliderIds),\n    aliveSpinnerIds: new Set<string>(aliveSpinnerIds),\n    hitCircleVerdict: { ...hitCircleVerdict },\n    sliderVerdict: { ...sliderVerdict },\n    checkPointVerdict: { ...checkPointVerdict },\n    currentTime: currentTime,\n    cursorPosition: cursorPosition,\n    latestHitObjectIndex: latestHitObjectIndex,\n    judgedObjects: [...judgedObjects],\n    clickWasUseful: clickWasUseful,\n    sliderBodyState: new Map<string, SliderBodyState>(sliderBodyState),\n    spinnerState: new Map<string, SpinnerState>(spinnerState),\n    pressingSince: pressingSince.slice(),\n  };\n}\n"
  },
  {
    "path": "libs/osu/core/src/gameplay/GameStateEvaluator.spec.ts",
    "content": "import { newPressingSince } from \"./GameStateEvaluator\";\nimport { NOT_PRESSING } from \"./GameState\";\nimport { OsuAction } from \"../replays/Replay\";\n\ndescribe(\"newPressingSince\", function () {\n  it(\"initial with new value at new click\", function () {\n    expect(newPressingSince([NOT_PRESSING, NOT_PRESSING], [OsuAction.leftButton], 10)).toEqual([10, NOT_PRESSING]);\n  });\n  it(\"keep the old value if still pressing\", function () {\n    expect(newPressingSince([30, NOT_PRESSING], [OsuAction.leftButton], 42)).toEqual([30, NOT_PRESSING]);\n  });\n  it(\"stay [NOT_PRESSING, NOT_PRESSING] when no action given\", function () {\n    expect(newPressingSince([NOT_PRESSING, NOT_PRESSING], [], -420)).toEqual([NOT_PRESSING, NOT_PRESSING]);\n  });\n  it(\"return to [NOT_PRESSING, NOT_PRESSING] when no action given\", function () {\n    expect(newPressingSince([320, 500], [], 600)).toEqual([NOT_PRESSING, NOT_PRESSING]);\n  });\n});\n"
  },
  {
    "path": "libs/osu/core/src/gameplay/GameStateEvaluator.ts",
    "content": "import { hitWindowsForOD, Position, Vec2 } from \"@osujs/math\";\nimport { Beatmap } from \"../beatmap/Beatmap\";\nimport { defaultGameState, GameState, HitCircleVerdict, NOT_PRESSING, PressingSinceTimings } from \"./GameState\";\nimport { isHitCircle, isSlider, isSpinner } from \"../hitobjects/Types\";\nimport { Slider } from \"../hitobjects/Slider\";\nimport { OsuAction, ReplayFrame } from \"../replays/Replay\";\nimport { MainHitObjectVerdict } from \"./Verdicts\";\nimport { HitCircle } from \"../hitobjects/HitCircle\";\nimport { RELAX_LENIENCY } from \"../mods/Mods\";\n\n/**\n * In the real osu game, the slider body will be evaluated at every game tick (?), which is something we can not do.\n *\n * The slider body tracking is only evaluated at important points:\n * * check points\n * * real frame times\n * Usually the delta between two frame times should be about ~16ms (~60FPS).\n * Occasionally there are outliers, which happened to me at testing (something like >40ms).\n *\n * Technically speaking\n */\n\n/**\n *  Evaluates the next game state based on the current one and the next frame.\n *\n *  Let the times of the frames be t[1...n]. osu! stores about 60FPS, this means\n *  that in between t[i] and t[i+1] there is no precise information available, which might be problematic for slider\n * checkpoint evaluations.\n *\n * So the following assumption is made for t[i] < t < t[i+1]:\n *  - If the player is still holding a click at t[i], then it's also at time t.\n *  - The position of the cursor at time t is (linearly) interpolated between t[i] and t[i+1].\n *\n */\n\ntype Event = {\n  time: number;\n  hitObjectId: string;\n  type:\n    | \"HIT_CIRCLE_SPAWN\"\n    | \"HIT_CIRCLE_FORCE_KILL\"\n    | \"SLIDER_START\"\n    | \"SLIDER_END\"\n    | \"SPINNER_START\"\n    | \"SPINNER_END\"\n    | \"SLIDER_CHECK_POINT\";\n};\n\nfunction generateEvents(beatmap: Beatmap, hitWindows: number[]): Event[] {\n  const events: Event[] = [];\n  const mehHitWindow = hitWindows[2];\n\n  const pushHitCircleEvents = (h: HitCircle) => {\n    events.push({ time: h.hitTime - h.approachDuration, hitObjectId: h.id, type: \"HIT_CIRCLE_SPAWN\" });\n    // TODO: In case you are allowed to press late -> +1 additional\n    events.push({ time: h.hitTime + mehHitWindow + 1, hitObjectId: h.id, type: \"HIT_CIRCLE_FORCE_KILL\" });\n  };\n\n  for (const h of beatmap.hitObjects) {\n    if (isHitCircle(h)) {\n      pushHitCircleEvents(h);\n    } else if (isSlider(h)) {\n      pushHitCircleEvents(h.head);\n      events.push({ time: h.startTime, hitObjectId: h.id, type: \"SLIDER_START\" });\n      events.push({ time: h.endTime, hitObjectId: h.id, type: \"SLIDER_END\" });\n      h.checkPoints.forEach((c) => {\n        events.push({ time: c.hitTime, hitObjectId: c.id, type: \"SLIDER_CHECK_POINT\" });\n      });\n    } else if (isSpinner(h)) {\n      events.push({ time: h.startTime, hitObjectId: h.id, type: \"SPINNER_START\" });\n      events.push({ time: h.endTime, hitObjectId: h.id, type: \"SPINNER_END\" });\n    }\n  }\n\n  // TODO: What if 2B maps?\n  events.sort((a, b) => a.time - b.time);\n\n  return events;\n}\n\nexport type NoteLockStyle = \"NONE\" | \"STABLE\" | \"LAZER\";\nexport type HitWindowStyle = \"OSU_STABLE\" | \"OSU_LAZER\";\n\nexport type GameStateEvaluatorOptions = {\n  hitWindowStyle: HitWindowStyle;\n  noteLockStyle: NoteLockStyle;\n};\n\nconst defaultOptions: GameStateEvaluatorOptions = {\n  noteLockStyle: \"STABLE\",\n  hitWindowStyle: \"OSU_STABLE\",\n};\n\nconst HitObjectVerdicts = {\n  GREAT: 0,\n  OK: 1,\n  MEH: 2,\n  MISS: 3,\n} as const;\n\nfunction isWithinHitWindow(hitWindow: number[], delta: number, verdict: MainHitObjectVerdict): boolean {\n  return Math.abs(delta) <= hitWindow[HitObjectVerdicts[verdict]];\n}\n\nexport class GameStateEvaluator {\n  private readonly events: Event[];\n  private gameState: GameState = defaultGameState();\n  private frame: ReplayFrame = { time: 0, position: { x: 0, y: 0 }, actions: [] };\n  private options: GameStateEvaluatorOptions;\n  private hitWindows: number[];\n\n  constructor(private readonly beatmap: Beatmap, options?: GameStateEvaluatorOptions) {\n    this.options = Object.assign({ ...defaultOptions }, options);\n    this.hitWindows = hitWindowsForOD(\n      beatmap.difficulty.overallDifficulty,\n      this.options.hitWindowStyle === \"OSU_LAZER\",\n    );\n    this.events = generateEvents(beatmap, this.hitWindows);\n  }\n\n  judgeHitCircle(id: string, verdict: HitCircleVerdict) {\n    this.gameState.hitCircleVerdict[id] = verdict;\n    this.gameState.aliveHitCircleIds.delete(id);\n    this.gameState.judgedObjects.push(id);\n  }\n\n  handleHitCircleSpawn(time: number, hitCircleId: string) {\n    this.gameState.aliveHitCircleIds.add(hitCircleId);\n  }\n\n  handleHitCircleForceKill(time: number, hitCircleId: string) {\n    // Already dead? The shinigami will just leave...\n    if (!this.gameState.aliveHitCircleIds.has(hitCircleId)) {\n      return;\n    }\n    // Otherwise we force kill for not being hit by the player ...\n    const verdict: HitCircleVerdict = { judgementTime: time, type: \"MISS\", missReason: \"TIME_EXPIRED\" };\n    this.judgeHitCircle(hitCircleId, verdict);\n  }\n\n  handleSliderStart(time: number, sliderId: string) {\n    this.gameState.aliveSliderIds.add(sliderId);\n  }\n\n  handleSliderEnding(time: number, sliderId: string) {\n    const slider = this.beatmap.getSlider(sliderId);\n\n    const headVerdict = this.gameState.hitCircleVerdict[slider.head.id];\n    // Clean up the head if it hasn't been interacted with the player in any way.\n    if (headVerdict === undefined) {\n      this.judgeHitCircle(slider.head.id, {\n        judgementTime: slider.endTime,\n        type: \"MISS\",\n        missReason: \"SLIDER_FINISHED_FASTER\",\n      });\n    }\n\n    // Now count the hit checkpoints and get the verdict\n    const totalCheckpoints = slider.checkPoints.length + 1;\n    let hitCheckpoints = 0;\n    if (!(headVerdict === undefined || headVerdict.type === \"MISS\")) hitCheckpoints++;\n    for (const c of slider.checkPoints) {\n      hitCheckpoints += this.gameState.checkPointVerdict[c.id]?.hit ? 1 : 0;\n    }\n    this.gameState.sliderVerdict[slider.id] = sliderVerdictBasedOnCheckpoints(totalCheckpoints, hitCheckpoints);\n    this.gameState.judgedObjects.push(slider.id);\n\n    // The head should not be alive\n    this.gameState.aliveSliderIds.delete(sliderId);\n    this.gameState.sliderBodyState.delete(sliderId);\n  }\n\n  predictedCursorPositionAt(time: number): Position {\n    const previousTime = this.gameState.currentTime;\n    const nextTime = this.frame.time;\n    const previousPosition = this.gameState.cursorPosition;\n    const nextPosition = this.frame.position;\n\n    if (previousTime === nextTime) return previousPosition;\n\n    const f = (time - previousTime) / (nextTime - previousTime);\n    return Vec2.interpolate(previousPosition, nextPosition, f);\n  }\n\n  handleSliderCheckPoint(time: number, id: string) {\n    const cursorPosition = this.predictedCursorPositionAt(time);\n    const checkPoint = this.beatmap.getSliderCheckPoint(id);\n    this.updateSliderBodyTracking(time, cursorPosition, this.gameState.pressingSince);\n    const sliderId = checkPoint.slider.id;\n    const state = this.gameState.sliderBodyState.get(sliderId);\n    if (state === undefined) {\n      throw Error(\"Somehow the slider body has no state while there is a checkpoint alive.\");\n    }\n    this.gameState.checkPointVerdict[id] = { hit: state.isTracking };\n    this.gameState.judgedObjects.push(id);\n  }\n\n  handleSpinnerStart(id: string) {\n    this.gameState.aliveSpinnerIds.add(id);\n  }\n\n  handleSpinnerEnd(id: string) {\n    this.gameState.aliveSpinnerIds.delete(id);\n    this.gameState.judgedObjects.push(id);\n  }\n\n  handleEvent(event: Event) {\n    const { hitObjectId, time, type } = event;\n    switch (type) {\n      case \"HIT_CIRCLE_SPAWN\":\n        this.handleHitCircleSpawn(time, hitObjectId);\n        break;\n      case \"HIT_CIRCLE_FORCE_KILL\":\n        this.handleHitCircleForceKill(time, hitObjectId);\n        break;\n      case \"SLIDER_START\":\n        this.handleSliderStart(time, hitObjectId);\n        break;\n      case \"SLIDER_END\":\n        this.handleSliderEnding(time, hitObjectId);\n        break;\n      case \"SLIDER_CHECK_POINT\":\n        this.handleSliderCheckPoint(time, hitObjectId);\n        break;\n      case \"SPINNER_START\":\n        this.handleSpinnerStart(hitObjectId);\n        break;\n      case \"SPINNER_END\":\n        this.handleSpinnerEnd(hitObjectId);\n        break;\n    }\n  }\n\n  handleAliveHitCircles() {\n    // There is only action if there is also a click in this frame ...\n    const hasRelax = this.beatmap.appliedMods.includes(\"RELAX\");\n    if (!this.hasFreshClickThisFrame && !hasRelax) {\n      return;\n    }\n    const { noteLockStyle } = this.options;\n    const currentTime = this.gameState.currentTime;\n    let noteLocked = false;\n\n    // JavaScript `Set` maintains its elements in insertion order so the early ones\n    // we iterate on are also the ones that are supposed to be hit first ...\n    // We copy because the values into an array because we might delete them ...\n    const hitCircleIds = Array.from(this.gameState.aliveHitCircleIds.values());\n\n    for (let i = 0; i < hitCircleIds.length; i++) {\n      const id = hitCircleIds[i];\n      const hitCircle = this.beatmap.getHitCircle(id);\n      const cursorInside = Vec2.withinDistance(\n        hitCircle.position,\n        this.gameState.cursorPosition,\n        Math.fround(hitCircle.radius),\n      );\n\n      if (!cursorInside) {\n        // We put a lock on the other circles because the first alive HitCircle is the only circle we can interact with.\n        if (noteLockStyle === \"STABLE\") {\n          noteLocked = true;\n        }\n        // It's a bit fairer because this allows us to force miss notes that are in the past.\n        if (noteLockStyle === \"LAZER\" && currentTime <= hitCircle.hitTime) {\n          noteLocked = true;\n        }\n        continue;\n      }\n\n      // If we got note locked, we want to set an animation then ignore the other hit circles\n      if (noteLocked) {\n        // TODO: Set state of `id` to be noteLocked at the current time (this allows us to show an \"shaking\" animation)\n        break;\n      }\n\n      // If this hitobject is too early for relax, then the other ones will be as well, so break.\n      const delta = currentTime - hitCircle.hitTime;\n      if (hasRelax && delta < -RELAX_LENIENCY) break;\n\n      let judged = false;\n      for (const verdict of [\"GREAT\", \"OK\", \"MEH\"] as const) {\n        if (isWithinHitWindow(this.hitWindows, delta, verdict)) {\n          this.judgeHitCircle(hitCircle.id, { judgementTime: currentTime, type: verdict });\n          judged = true;\n          break;\n        }\n      }\n\n      // TODO: Force miss other notes less than i for lazer style\n      if (judged) break;\n\n      if (isWithinHitWindow(this.hitWindows, delta, \"MISS\")) {\n        // TODO: Add a \"HIT_TOO_LATE\" (even though it's kinda unfair, but this is osu!stable behavior)\n        // For some reason in osu!stable the HitCircle that has a MEH time of let's say +-109.5ms is still alive at\n        // t+110ms and can be \"clicked\" by the user at time t+110ms, but it will just result in a miss. The problem is\n        // that the underlying note will then be ignored because the click is \"wasted\" for the already expired hit\n        // circle. => This might be a stable bug or feature?\n        this.judgeHitCircle(hitCircle.id, { judgementTime: currentTime, type: \"MISS\", missReason: \"HIT_TOO_EARLY\" });\n        judged = true;\n      }\n\n      // TODO: Do we force miss other notes as well? for lazer style\n      if (judged) break;\n    }\n  }\n\n  private get hasFreshClickThisFrame(): boolean {\n    return this.gameState.pressingSince.includes(this.gameState.currentTime);\n  }\n\n  private headHitTime(headId: string): number | undefined {\n    const verdict = this.gameState.hitCircleVerdict[headId];\n    if (!verdict || verdict.type === \"MISS\") return undefined;\n    return verdict.judgementTime;\n  }\n\n  private updateSliderBodyTracking(time: number, cursorPosition: Position, pressingSince: PressingSinceTimings): void {\n    for (const id of this.gameState.aliveSliderIds) {\n      const slider = this.beatmap.getSlider(id);\n\n      const headHitTime: number | undefined = this.headHitTime(slider.head.id);\n      const wasTracking: boolean = this.gameState.sliderBodyState.get(id)?.isTracking ?? false;\n      const hasRelax = this.beatmap.appliedMods.includes(\"RELAX\");\n      const isTracking = determineTracking(\n        wasTracking,\n        slider,\n        cursorPosition,\n        time,\n        pressingSince,\n        headHitTime,\n        hasRelax,\n      );\n      this.gameState.sliderBodyState.set(id, { isTracking });\n    }\n  }\n\n  // Process the events until event[i].time <= maxTimeInclusive is no longer valid.\n  handleEventsUntilTime(maxTimeInclusive: number) {\n    const { gameState, events } = this;\n    while (gameState.eventIndex < events.length) {\n      const event = events[gameState.eventIndex];\n      if (event.time > maxTimeInclusive) {\n        break;\n      }\n      gameState.eventIndex += 1;\n      this.handleEvent(event);\n    }\n  }\n\n  evaluate(gameState: GameState, frame: ReplayFrame) {\n    this.gameState = gameState;\n    this.frame = frame;\n\n    // 1. Deal with hit objects that are only affected with movement (sliders, spinners)\n    // Tbh in my first version I have this.handleEventsUntilTime(frame.time) right now, which makes more sense.\n\n    this.handleEventsUntilTime(frame.time - 1);\n\n    // 2. Now consider things that get affected by releasing / clicking at this particular time.\n    this.gameState.cursorPosition = frame.position;\n    this.gameState.currentTime = frame.time;\n    this.gameState.pressingSince = newPressingSince(this.gameState.pressingSince, frame.actions, frame.time);\n    this.gameState.clickWasUseful = false;\n\n    this.handleAliveHitCircles();\n    this.updateSliderBodyTracking(frame.time, frame.position, this.gameState.pressingSince);\n\n    // 3. Deal with events after the click such as force killing a HitCircle\n    this.handleEventsUntilTime(frame.time);\n  }\n}\n\nconst sliderProgress = (slider: Slider, time: number) => (time - slider.startTime) / slider.duration;\n\n/**\n * SliderTracking is described in a complicated way in osu!lazer, but it can be boiled down to:\n *\n * * A key must be pressed (?)\n * * Slider tracking is only done between slider.startTime (inclusively) and slider.endTime\n * (exclusively).\n * * The follow circle is scaled up to 2.4 if tracking, and down to 1.0 if not tracking, the cursor should be\n * in the follow circle.\n * * Additionally there are two states of a slider:\n *  - Either the header was not hit, then we can accept any key for slider tracking.\n *\n *  - If the head was hit at `t`, then we can only restrict the keys to \"fresh\" clicks, which means clicks not\n * before t.\n *\n * Note that the state can be 1. at first and then transition to 2.\n *\n * In osu!lazer the tracking follows the visual tracking:\n * https://discord.com/channels/188630481301012481/188630652340404224/865648740810883112\n * https://github.com/ppy/osu/blob/6cec1145e3510eb27c6fbeb0f93967d2d872e600/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs#L61\n * The slider ball actually gradually scales to 2.4 (duration: 300ms, method: Easing.OutQuint) which means that at the\n * beginning the cursor has less leeway than after 300ms, while in osu!stable you instantly have the maximum leeway. In\n * osu!lazer it's actually a little bit harder than osu!stable.\n */\n\nfunction determineTracking(\n  previouslyTracking: boolean,\n  slider: Slider,\n  cursorPosition: Position,\n  time: number,\n  pressingSince: PressingSinceTimings,\n  headHitTime?: number,\n  hasRelax?: boolean,\n): boolean {\n  const keyIsBeingPressed = pressingSince.findIndex((x) => x !== NOT_PRESSING) >= 0;\n  // Zeroth condition\n  if (!keyIsBeingPressed && !hasRelax) return false;\n\n  // First condition\n  if (time < slider.startTime || slider.endTime <= time) return false;\n\n  // Second condition\n  const progress = sliderProgress(slider, time);\n  const followCircleRadius = (previouslyTracking ? 2.4 : 1.0) * slider.radius;\n  const distanceCursorToBall = Vec2.distance(slider.ballPositionAt(progress), cursorPosition);\n  if (distanceCursorToBall > followCircleRadius) return false;\n\n  // Now last condition\n  // State 1\n  if (headHitTime === undefined) return true; // Since any key is ok\n  // For the click that was done at t=headHitTime: t >= headHitTime is true.\n  // In the other case, we require a fresh click\n  // State 2 (requiring a fresh click)\n  return pressingSince.findIndex((x) => x >= headHitTime) >= 0;\n}\n\nfunction sliderVerdictBasedOnCheckpoints(totalCheckpoints: number, hitCheckpoints: number): MainHitObjectVerdict {\n  if (hitCheckpoints === totalCheckpoints) return \"GREAT\";\n  if (hitCheckpoints === 0) return \"MISS\";\n  if (hitCheckpoints * 2 >= totalCheckpoints) return \"OK\";\n  return \"MEH\";\n}\n\n// Maybe hitObjects should be flattened out (nested pulled out)\n// The mods should be applied to those them ...\nconst actionsToBooleans = (osuActions: OsuAction[]) => [\n  osuActions.includes(OsuAction.leftButton),\n  osuActions.includes(OsuAction.rightButton),\n];\n\nexport const newPressingSince = (pressingSince: PressingSinceTimings, osuActions: OsuAction[], time: number) => {\n  const pressed = actionsToBooleans(osuActions);\n  const newPressingSince = [...pressingSince];\n  for (let i = 0; i < newPressingSince.length; i++) {\n    if (pressed[i]) {\n      newPressingSince[i] = Math.min(newPressingSince[i], time);\n    } else {\n      newPressingSince[i] = NOT_PRESSING;\n    }\n  }\n  return newPressingSince;\n};\n"
  },
  {
    "path": "libs/osu/core/src/gameplay/GameStateTimeMachine.ts",
    "content": "import { cloneGameState, defaultGameState, GameState } from \"./GameState\";\nimport { ReplayFrame } from \"../replays/Replay\";\nimport { Vec2 } from \"@osujs/math\";\nimport { Beatmap } from \"../beatmap/Beatmap\";\nimport { GameStateEvaluator, GameStateEvaluatorOptions } from \"./GameStateEvaluator\";\n\nexport interface GameStateTimeMachine {\n  gameStateAt(time: number): GameState;\n}\n\n/**\n * By default O(R sqrt n) memory and O(sqrt n) time, where R is the size of a replay state.\n * Stores replays at the indices [0, sqrt n, 2 *sqrt n, ..., sqrt n * sqrt n] and the others are inferred.\n *\n * TODO: We could do caching like described in method 4 of\n * https://gamedev.stackexchange.com/questions/6080/how-to-design-a-replay-system/8372#8372 TODO: Should we\n * Object.freeze(...) the cached ones in order to prevent accidental mutations?\n */\nexport class BucketedGameStateTimeMachine implements GameStateTimeMachine {\n  // 0 stands for initial state\n  // and i means that i frames have been processed\n\n  currentIndex = 0;\n  currentGameState: GameState;\n  storedGameState: GameState[] = [];\n  bucketSize: number;\n  frames: ReplayFrame[];\n  evaluator: GameStateEvaluator;\n\n  constructor(\n    initialKnownFrames: ReplayFrame[],\n    private readonly beatmap: Beatmap,\n    // private readonly hitObjects: OsuHitObject[],\n    private readonly settings: GameStateEvaluatorOptions,\n    bucketSize?: number,\n  ) {\n    // Add a dummy replay frame at the beginning.\n    this.frames = [{ time: -727_727, position: new Vec2(0, 0), actions: [] }, ...initialKnownFrames];\n    this.bucketSize = bucketSize ?? Math.ceil(Math.sqrt(this.frames.length));\n    this.storedGameState[0] = defaultGameState();\n    this.currentGameState = cloneGameState(this.storedGameState[0]);\n    this.evaluator = new GameStateEvaluator(beatmap, settings);\n  }\n\n  private getHighestCachedIndex(time: number): number {\n    for (let i = 0; i < this.frames.length; i += this.bucketSize) {\n      // The second condition should not happen in our version of implementation where we travel forward.\n      if (time < this.frames[i].time || !this.storedGameState[i]) {\n        return i - this.bucketSize;\n      }\n    }\n    return 0;\n  }\n\n  gameStateAt(time: number): GameState {\n    const highestCachedIndex = this.getHighestCachedIndex(time);\n\n    // TODO: Just check if we had normal forward behavior or not, this can drastically improve performance\n    // If not, we need to reset something such as the derived data.\n\n    // Either we have to travel back anyways or there is a future index available for that time.\n    if (this.currentIndex < highestCachedIndex || time < this.frames[this.currentIndex].time) {\n      this.currentIndex = highestCachedIndex;\n      this.currentGameState = cloneGameState(this.storedGameState[this.currentIndex]);\n    }\n\n    // Check if we need to move forward in time\n    while (this.currentIndex + 1 < this.frames.length) {\n      const nextFrame = this.frames[this.currentIndex + 1];\n      if (time < nextFrame.time) {\n        break;\n      }\n\n      this.evaluator.evaluate(this.currentGameState, nextFrame);\n      this.currentIndex += 1;\n\n      // Caching the state at a multiple of bucketSize\n      if (this.currentIndex % this.bucketSize === 0) {\n        this.storedGameState[this.currentIndex] = cloneGameState(this.currentGameState);\n      }\n    }\n    return this.currentGameState;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/gameplay/GameplayAnalysisEvent.ts",
    "content": "import { Position } from \"@osujs/math\";\nimport { normalizeHitObjects } from \"../utils\";\nimport { GameState } from \"./GameState\";\nimport { Slider } from \"../hitobjects/Slider\";\nimport { HitCircle } from \"../hitobjects/HitCircle\";\nimport { MainHitObjectVerdict } from \"./Verdicts\";\nimport { OsuHitObject } from \"../hitobjects/Types\";\n\n/**\n * ReplayAnalysisEvents are point of interests for the user.\n *\n */\n\nexport type GameplayAnalysisEventType = \"HitObjectJudgement\" | \"CheckpointJudgement\" | \"UnnecessaryClick\";\n\n// Where and when to display these events\ninterface DisplayBase {\n  type: GameplayAnalysisEventType;\n  time: number;\n  position: Position;\n}\n\n// TODO: Export\ntype HitObjectVerdict = MainHitObjectVerdict;\n\n// Those events are to be displayed on the screen\n\nexport interface HitObjectJudgement extends DisplayBase {\n  type: \"HitObjectJudgement\";\n  verdict: HitObjectVerdict;\n  hitObjectId: string;\n  // Because people might want to prefer not showing slider head judgements\n  // In Lazer they are shown\n  isSliderHead?: boolean;\n}\n\nexport interface CheckpointJudgement extends DisplayBase {\n  type: \"CheckpointJudgement\";\n  hit: boolean;\n  // Usually not the causing factor of a slider break\n  isLastTick?: boolean;\n}\n\n// Used in the future, but just shows that these events are extendable.\nexport interface UnnecessaryClick extends DisplayBase {\n  type: \"UnnecessaryClick\";\n}\n\nexport type ReplayAnalysisEvent = HitObjectJudgement | CheckpointJudgement | UnnecessaryClick;\n\n// Type predicates\nexport const isHitObjectJudgement = (h: ReplayAnalysisEvent): h is HitObjectJudgement =>\n  h.type === \"HitObjectJudgement\";\n\n// This is osu!stable style and is also only recommended for offline processing.\n// In the future, where something like online replay streaming is implemented, this implementation will ofc be too slow.\nexport function retrieveEvents(gameState: GameState, hitObjects: OsuHitObject[]) {\n  const events: ReplayAnalysisEvent[] = [];\n  const dict = normalizeHitObjects(hitObjects);\n\n  // HitCircle judgements (SliderHeads included and indicated)\n  for (const id in gameState.hitCircleVerdict) {\n    const state = gameState.hitCircleVerdict[id];\n    const hitCircle = dict[id] as HitCircle;\n    const isSliderHead = hitCircle.sliderId !== undefined;\n    const verdict = state.type;\n    events.push({\n      type: \"HitObjectJudgement\",\n      time: state.judgementTime,\n      hitObjectId: id,\n      position: hitCircle.position,\n      verdict,\n      isSliderHead,\n    });\n  }\n\n  for (const id in gameState.sliderVerdict) {\n    const verdict = gameState.sliderVerdict[id];\n    // Slider judgement events\n    const slider = dict[id] as Slider;\n    const position = slider.endPosition;\n    events.push({ time: slider.endTime, hitObjectId: id, position, verdict, type: \"HitObjectJudgement\" });\n\n    // CheckpointEvents\n    for (const point of slider.checkPoints) {\n      const checkPointState = gameState.checkPointVerdict[point.id];\n      const hit = checkPointState?.hit ?? false;\n\n      const isLastTick = point.type === \"LAST_LEGACY_TICK\";\n      events.push({ time: slider.endTime, position: point.position, type: \"CheckpointJudgement\", hit, isLastTick });\n    }\n  }\n\n  // TODO: Spinner nested ticks events\n\n  return events;\n}\n"
  },
  {
    "path": "libs/osu/core/src/gameplay/GameplayInfo.ts",
    "content": "/**\n * Shows the statistics\n */\n\nimport { GameState } from \"./GameState\";\nimport { HitObjectType, SliderCheckPointType } from \"../hitobjects/Types\";\nimport { MainHitObjectVerdict } from \"./Verdicts\";\nimport { Beatmap } from \"../beatmap/Beatmap\";\nimport { HitCircle } from \"../hitobjects/HitCircle\";\nimport { Slider } from \"../hitobjects/Slider\";\nimport { SliderCheckPoint } from \"../hitobjects/SliderCheckPoint\";\n\nexport interface GameplayInfo {\n  accuracy: number;\n  // osu!stable: 300, 100, 50, 0\n  // osu!lazer:  300, 100, 50, 25, 10, 0 (or something like that, small ticks are from sliders and count towards acc)\n  verdictCounts: number[];\n  score: number;\n  currentCombo: number;\n  maxComboSoFar: number;\n}\n\n/** COMBO **/\n\ninterface ReplayComboInformation {\n  currentCombo: number;\n  maxComboSoFar: number;\n}\n\ntype ReplayJudgementCounts = number[];\n\nfunction updateComboInfo(combo: ReplayComboInformation, type: HitObjectType | SliderCheckPointType, hit: boolean) {\n  let currentCombo = combo.currentCombo;\n  switch (type) {\n    case \"HIT_CIRCLE\":\n    case \"TICK\":\n    case \"REPEAT\":\n    case \"SPINNER\":\n      currentCombo = hit ? currentCombo + 1 : 0;\n      break;\n\n    case \"LAST_LEGACY_TICK\":\n      // Slider ends do not break the combo, but they can increase them\n      currentCombo += hit ? 1 : 0;\n      break;\n\n    case \"SLIDER\":\n      // For sliders there is no combo update\n      break;\n  }\n\n  return { currentCombo, maxComboSoFar: Math.max(combo.maxComboSoFar, currentCombo) };\n}\n\n/** ACC **/\n\n/**\n * Returns a number between 0 and 1 using osu!stable accuracy logic.\n * Also returns undefined if there is no count\n *\n * @param count counts of 300, 100, 50, 0 (in this order)\n */\nexport function osuStableAccuracy(count: number[]): number | undefined {\n  if (count.length !== 4) {\n    return undefined;\n  }\n  const JudgementScores = [300, 100, 50, 0];\n\n  let perfect = 0,\n    actual = 0;\n  for (let i = 0; i < count.length; i++) {\n    actual += JudgementScores[i] * count[i];\n    perfect += JudgementScores[0] * count[i];\n  }\n\n  if (perfect === 0) {\n    return undefined;\n  }\n\n  return actual / perfect;\n}\n\n/** SCORE **/\n\ninterface EvaluationOption {\n  scoringSystem: \"ScoreV1\" | \"ScoreV2\";\n  // maybe beatmap difficulty -> since they are required for score v1 calc\n}\n\n//https://osu.ppy.sh/wiki/en/Score/ScoreV1\n\nconst defaultEvaluationOptions = {\n  scoringSystem: \"ScoreV1\",\n} as EvaluationOption;\n\ntype StableVerdictCount = Record<MainHitObjectVerdict, number>;\n\nexport const defaultGameplayInfo: GameplayInfo = Object.freeze({\n  currentCombo: 0,\n  maxComboSoFar: 0,\n  verdictCounts: [0, 0, 0, 0],\n  accuracy: 0,\n  score: 0,\n});\n\n/**\n * Calculating: Count, Accuracy, Combo, MaxCombo\n * The one who is calling this has to make sure that slider heads are not considered in case they are using osu!stable\n * calculation.\n */\nexport class GameplayInfoEvaluator {\n  options: EvaluationOption;\n  judgedObjectsIndex: number;\n  comboInfo: ReplayComboInformation;\n  verdictCount: StableVerdictCount;\n\n  constructor(private beatmap: Beatmap, options?: Partial<EvaluationOption>) {\n    this.options = { ...defaultEvaluationOptions, ...options };\n    this.comboInfo = { maxComboSoFar: 0, currentCombo: 0 };\n    this.verdictCount = { MISS: 0, MEH: 0, GREAT: 0, OK: 0 };\n    this.judgedObjectsIndex = 0;\n    // TODO: Do some initialization for calculating ScoreV2 (like max score)\n  }\n\n  evaluateHitObject(hitObjectType: HitObjectType, verdict: MainHitObjectVerdict, isSliderHead?: boolean) {\n    this.comboInfo = updateComboInfo(this.comboInfo, hitObjectType, verdict !== \"MISS\");\n    if (!isSliderHead) {\n      this.verdictCount[verdict] += 1;\n    }\n  }\n\n  evaluateSliderCheckpoint(hitObjectType: SliderCheckPointType, hit: boolean) {\n    this.comboInfo = updateComboInfo(this.comboInfo, hitObjectType, hit);\n  }\n\n  countAsArray() {\n    return ([\"GREAT\", \"OK\", \"MEH\", \"MISS\"] as const).map((v) => this.verdictCount[v]);\n  }\n\n  evaluateReplayState(replayState: GameState): GameplayInfo {\n    // Assume something like seeking backwards happened at reevaluate\n    if (this.judgedObjectsIndex >= replayState.judgedObjects.length + 1) {\n      this.comboInfo = { maxComboSoFar: 0, currentCombo: 0 };\n      this.verdictCount = { MISS: 0, MEH: 0, GREAT: 0, OK: 0 };\n      this.judgedObjectsIndex = 0;\n    }\n\n    while (this.judgedObjectsIndex < replayState.judgedObjects.length) {\n      const id = replayState.judgedObjects[this.judgedObjectsIndex++];\n      const hitObject = this.beatmap.getHitObject(id);\n      if (hitObject instanceof SliderCheckPoint) {\n        const hit = replayState.checkPointVerdict[hitObject.id].hit;\n        this.evaluateSliderCheckpoint(hitObject.type, hit);\n      } else if (hitObject instanceof HitCircle) {\n        const verdict = replayState.hitCircleVerdict[id].type;\n        const isSliderHead = hitObject.sliderId !== undefined;\n        this.evaluateHitObject(hitObject.type, verdict, isSliderHead);\n      } else if (hitObject instanceof Slider) {\n        const verdict = replayState.sliderVerdict[hitObject.id];\n        this.evaluateHitObject(hitObject.type, verdict);\n      } else {\n        // Spinner\n        // TODO: We just going to assume that they hit it\n        this.evaluateHitObject(\"SPINNER\", \"GREAT\");\n      }\n    }\n\n    const counts = this.countAsArray();\n    return {\n      score: 0,\n      verdictCounts: counts,\n      accuracy: osuStableAccuracy(counts) ?? 1.0,\n      currentCombo: this.comboInfo.currentCombo,\n      maxComboSoFar: this.comboInfo.maxComboSoFar,\n    };\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/gameplay/Verdicts.ts",
    "content": "export type MainHitObjectVerdict = \"GREAT\" | \"OK\" | \"MEH\" | \"MISS\";\nexport type SliderCheckpointVerdict = \"HIT\" | \"MISS\";\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/HitCircle.ts",
    "content": "import { Position } from \"@osujs/math\";\nimport { HitObjectType } from \"./Types\";\nimport { HasHitTime, HasId, HasPosition, HasSpawnTime } from \"./Properties\";\n\nexport class HitCircle implements HasId, HasPosition, HasHitTime, HasSpawnTime {\n  static OBJECT_RADIUS = 64;\n\n  id = \"\";\n  hitTime = 0;\n  approachDuration = 0;\n\n  comboSetIndex = 0;\n  withinComboSetIndex = 0;\n\n  scale = 1;\n  position: Position = { x: 0, y: 0 };\n\n  // Only used because there's a bug in the Flashlight difficulty processing\n  unstackedPosition: Position = { x: 0, y: 0 };\n\n  sliderId?: string;\n\n  get type(): HitObjectType {\n    return \"HIT_CIRCLE\";\n  }\n\n  get radius(): number {\n    return HitCircle.OBJECT_RADIUS * this.scale;\n  }\n\n  get spawnTime(): number {\n    return this.hitTime - this.approachDuration;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/Properties.ts",
    "content": "import { Position } from \"@osujs/math\";\n\nexport interface HasPosition {\n  position: Position;\n}\n\nexport interface HasHitTime {\n  hitTime: number;\n}\n\nexport interface HasSpawnTime {\n  spawnTime: number;\n}\n\nexport interface HasId {\n  id: string;\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/Slider.ts",
    "content": "import { SliderPath } from \"./slider/SliderPath\";\nimport { Position, Vec2 } from \"@osujs/math\";\nimport { HitCircle } from \"./HitCircle\";\nimport { SliderCheckPoint } from \"./SliderCheckPoint\";\nimport { HasId } from \"./Properties\";\nimport { HitObjectType } from \"./Types\";\n\nexport class Slider implements HasId {\n  // scoring distance with a speed-adjusted beat length of 1 second (i.e. the speed slider balls\n  // move through their track).\n  static BASE_SCORING_DISTANCE = 100;\n\n  id = \"\";\n\n  head: HitCircle;\n\n  path: SliderPath = new SliderPath([]);\n  checkPoints: SliderCheckPoint[] = [];\n  velocity = 1;\n  tickDistance = 0;\n  tickDistanceMultiplier = 1;\n  legacyLastTickOffset?: number;\n  repeatCount = 0;\n\n  constructor(hitCircle: HitCircle) {\n    this.head = hitCircle;\n  }\n\n  get type(): HitObjectType {\n    return \"SLIDER\";\n  }\n\n  get spawnTime(): number {\n    return this.head.spawnTime;\n  }\n\n  // Number of times the slider spans over the screen.\n  get spanCount(): number {\n    return this.repeatCount + 1;\n  }\n\n  get scale(): number {\n    return this.head.scale;\n  }\n\n  get radius(): number {\n    return this.head.radius;\n  }\n\n  get spanDuration(): number {\n    return this.duration / this.spanCount;\n  }\n\n  get duration(): number {\n    return this.endTime - this.startTime;\n  }\n\n  get startTime(): number {\n    return this.head.hitTime;\n  }\n\n  get endTime(): number {\n    return this.startTime + (this.spanCount * this.path.distance) / this.velocity;\n  }\n\n  get startPosition(): Position {\n    return this.head.position;\n  }\n\n  get endPosition(): Position {\n    // TODO: Caching like in osu!lazer since this takes a lot of time\n    return Vec2.add(this.head.position, this.ballOffsetAt(1.0));\n  }\n\n  get unstackedEndPosition(): Position {\n    return Vec2.add(this.head.unstackedPosition, this.ballOffsetAt(1.0));\n  }\n\n  /**\n   * Returns the absolute position of the ball given the progress p, where p is the percentage of time passed\n   * between startTime and endTime.\n   * @param progress\n   */\n  ballPositionAt(progress: number): Position {\n    return Vec2.add(this.head.position, this.ballOffsetAt(progress));\n  }\n\n  /**\n   * Returns the position given the (time) progress. Basically it just tells you where the slider ball should be after\n   * p% of time has passed.\n   *\n   * @param progress number between 0 and 1 determining the time progress\n   */\n  ballOffsetAt(progress: number): Position {\n    const spanProgress = progress * this.spanCount;\n    let progressInSpan = spanProgress % 1.0;\n    // When it's \"returning\" we should consider the progress in an inverted way.\n    if (Math.floor(spanProgress) % 2 === 1) {\n      progressInSpan = 1.0 - progressInSpan;\n    }\n    return this.path.positionAt(progressInSpan);\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/SliderCheckPoint.ts",
    "content": "import { Slider } from \"./Slider\";\nimport { Position, Vec2 } from \"@osujs/math\";\nimport { HasHitTime, HasId, HasPosition } from \"./Properties\";\nimport { SliderCheckPointType } from \"./Types\";\n\n// Important points on the slider. Depending on if they were \"hit\" or not, we will have a different judgement on the\n// slider.\nexport class SliderCheckPoint implements HasId, HasPosition, HasHitTime {\n  constructor(public readonly slider: Slider) {}\n\n  id = \"\";\n  type: SliderCheckPointType = \"TICK\";\n  spanIndex = 0;\n  // The `spanProgress` is a number between 0 and 1 that determines the position on the slider path.\n  spanProgress = 0;\n  spanStartTime = 0;\n  offset: Position = { x: 0, y: 0 };\n\n  hitTime = 0;\n\n  get position(): Position {\n    return Vec2.add(this.slider.startPosition, this.offset);\n  }\n\n  get scale(): number {\n    return this.slider.scale;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/Spinner.ts",
    "content": "import { HasId, HasSpawnTime } from \"./Properties\";\n\n/**\n * Logic:\n *\n * * Assumption is that the user can't spin more than 8 times per second `max_rotations_per_second=8`.\n * * \"MaxSpinsPossible\" \"MinSpinsNeeded\"\n *\n *\n * Then there are like \"MaxSpinsPossible\" spinner ticks generated (???) which have a start time of [0, d, 2d, ...,\n * maxSpinsPossible * d] where d is the duration of one spin.\n *\n * 100% min spinned -> GREAT\n * > 90%-> OK\n * > 75% -> MEH\n * otherwise miss\n * requires gameClock playRate\n *\n *\n *                 if (HitObject.SpinsRequired == 0)\n // some spinners are so short they can't require an integer spin count.\n // these become implicitly hit.\n return 1;\n * SPM count is calculated as follows:\n *\n * First define a time window spm_count_duration = 595ms for example then find out how much you have spinned at t\n * compared to t - spm_count_duration.\n *\n * This is done by keeping track with a queue of (t, total_rotation) by most people\n *\n */\n// Sources: https://github.com/itdelatrisu/opsu/blob/master/src/itdelatrisu/opsu/objects/Spinner.java\n// https://github.com/McKay42/McOsu/blob/master/src/App/Osu/OsuSpinner.cpp\n// https://github.com/ppy/osu/blob/master/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs\n\nfunction diffRange(difficulty: number, min: number, mid: number, max: number) {\n  if (difficulty > 5) return mid + ((max - mid) * (difficulty - 5)) / 5;\n  if (difficulty < 5) return mid - ((mid - min) * (5 - difficulty)) / 5;\n\n  return mid;\n}\n\nexport class Spinner implements HasId, HasSpawnTime {\n  id = \"\";\n  startTime = 0;\n  duration = 0;\n  spinsRequired = 1;\n  maximumBonusSpins = 1;\n\n  // The spinner is visible way earlier, but can only be interacted with at [startTime, endTime]\n  get spawnTime(): number {\n    return this.startTime;\n  }\n\n  get endTime(): number {\n    return this.startTime + this.duration;\n  }\n\n  get type() {\n    return \"SPINNER\";\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/Types.ts",
    "content": "import { Spinner } from \"./Spinner\";\nimport { HitCircle } from \"./HitCircle\";\nimport { Slider } from \"./Slider\";\nimport { SliderCheckPoint } from \"./SliderCheckPoint\";\n\nexport type HitObjectType = \"HIT_CIRCLE\" | \"SLIDER\" | \"SPINNER\";\nexport type SliderCheckPointType = \"TICK\" | \"REPEAT\" | \"LAST_LEGACY_TICK\";\n\n// TODO: Change naming\nexport type MainHitObject = Spinner | HitCircle | Slider;\nexport type OsuHitObject = MainHitObject;\nexport type AllHitObjects = MainHitObject | SliderCheckPoint;\n\nexport const isHitCircle = (o: MainHitObject): o is HitCircle => o.type === \"HIT_CIRCLE\";\nexport const isSlider = (o: MainHitObject): o is Slider => o.type === \"SLIDER\";\nexport const isSpinner = (o: MainHitObject): o is Spinner => o.type === \"SPINNER\";\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/slider/PathApproximator.ts",
    "content": "import { float32, float32_add, float32_div, float32_mul, floatEqual, Vec2 } from \"@osujs/math\";\n\n// TODO: Move to osu-math\n// https://github.com/ppy/osu-framework/blob/f9d44b1414e30ad507894ef7eaaf5d1b0118be82/osu.Framework/Utils/PathApproximator.cs\n\nconst bezierTolerance = Math.fround(0.25);\nconst circularArcTolerance = Math.fround(0.1);\n// The amount of pieces to calculate for each control point quadruplet.\nconst catmullDetail = 50;\n\ninterface CircularArcProperties {\n  thetaStart: number;\n  thetaRange: number;\n  direction: number;\n  radius: number;\n  center: Vec2;\n}\n\nconst toFloat = (x: number) => Math.fround(x);\n\n/**\n * Helper methods to approximate a path by interpolating a sequence of control points.\n */\nexport class PathApproximator {\n  /**\n   * Creates a piecewise-linear approximation of a bezier curve, by adaptively repeatedly subdividing\n   * the control points until their approximation error vanishes below a given threshold.\n   * @returns A list of vectors representing the piecewise-linear approximation.\n   */\n  static approximateBezier(controlPoints: Vec2[], p = 0): Vec2[] {\n    const output: Vec2[] = [];\n    const n = controlPoints.length - 1;\n\n    if (n < 0) {\n      return output;\n    }\n    // Both Stacks<>\n    const toFlatten: Vec2[][] = [];\n    const freeBuffers: Vec2[][] = [];\n\n    // Creates a copy of controlPoints\n    const points = [...controlPoints];\n\n    if (p > 0 && p < n) {\n      for (let i = 0; i < n - p; i++) {\n        const subBezier: Vec2[] = new Array(p + 1);\n        subBezier[0] = points[i];\n        for (let j = 0; j < p - 1; j++) {\n          subBezier[j + 1] = points[i + 1];\n          for (let k = 1; k < p - j; k++) {\n            const l = Math.min(k, n - p - i);\n            points[i + k] = points[i + k]\n              .scale(l)\n              .add(points[i + k + 1])\n              .scale(1 / (l + 1));\n          }\n        }\n        subBezier[p] = points[i + 1];\n        toFlatten.push(subBezier);\n      }\n\n      // TODO: Is this same as  points[(n-p)..]) as in C# ?\n      toFlatten.push(points.slice(n - p, points.length));\n      // TODO: Is this as in the osu!lazer code?\n      // Reverse the stack so elements can be accessed in order\n      toFlatten.reverse();\n    } else {\n      // B-spline subdivisions unnecessary, degenerate to single bezier\n      p = n;\n      toFlatten.push(points);\n    }\n    const subdivisionBuffer1: Vec2[] = new Array(p + 1);\n    const subdivisionBuffer2: Vec2[] = new Array(p * 2 + 1);\n    const leftChild: Vec2[] = subdivisionBuffer2;\n\n    // let leftChild = subdivisionBuffer2;\n\n    while (toFlatten.length > 0) {\n      // Can't be undefined dude\n      const parent: Vec2[] = toFlatten.pop() as Vec2[];\n\n      if (PathApproximator._bezierIsFlatEnough(parent)) {\n        // If the control points we currently operate on are sufficiently \"flat\", we use\n        // an extension to De Casteljau's algorithm to obtain a piecewise-linear approximation\n        // of the bezier curve represented by our control points, consisting of the same amount\n        // of points as there are control points.\n        PathApproximator._bezierApproximate(parent, output, subdivisionBuffer1, subdivisionBuffer2, p + 1);\n\n        freeBuffers.push(parent);\n        continue;\n      }\n\n      // If we do not yet have a sufficiently \"flat\" (in other words, detailed) approximation we keep\n      // subdividing the curve we are currently operating on.\n      const rightChild: Vec2[] = freeBuffers.pop() ?? new Array(p + 1);\n\n      PathApproximator._bezierSubdivide(parent, leftChild, rightChild, subdivisionBuffer1, p + 1);\n\n      // We re-use the buffer of the parent for one of the children, so that we save one allocation per iteration.\n      for (let i = 0; i < p + 1; ++i) {\n        parent[i] = leftChild[i];\n      }\n\n      toFlatten.push(rightChild);\n      toFlatten.push(parent);\n    }\n\n    output.push(controlPoints[n]);\n\n    return output;\n  }\n\n  /**\n   * Creates a piecewise-linear approximation of a Catmull-Rom spline.\n   * @returns A list of vectors representing the piecewise-linear approximation.\n   */\n  static approximateCatmull(controlPoints: Vec2[]): Vec2[] {\n    const result: Vec2[] = [];\n    const controlPointsLength = controlPoints.length;\n\n    for (let i = 0; i < controlPointsLength - 1; i++) {\n      const v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i];\n      const v2 = controlPoints[i];\n      const v3 = i < controlPointsLength - 1 ? controlPoints[i + 1] : v2.add(v2).sub(v1);\n      const v4 = i < controlPointsLength - 2 ? controlPoints[i + 2] : v3.add(v3).sub(v2);\n\n      for (let c = 0; c < catmullDetail; c++) {\n        result.push(PathApproximator._catmullFindPoint(v1, v2, v3, v4, Math.fround(c / catmullDetail)));\n        result.push(PathApproximator._catmullFindPoint(v1, v2, v3, v4, Math.fround((c + 1) / catmullDetail)));\n      }\n    }\n\n    return result;\n  }\n\n  // TODO: Use Math.fround maybe\n  static circularArcProperties(controlPoints: Vec2[]): CircularArcProperties | undefined {\n    const a = controlPoints[0];\n    const b = controlPoints[1];\n    const c = controlPoints[2];\n    if (floatEqual(0, float32(float32_mul(b.y - a.y, c.x - a.x) - float32_mul(b.x - a.x, c.y - a.y))))\n      return undefined; // = invalid\n\n    const d = float32_mul(2, float32_add(float32_add(float32_mul(a.x, b.sub(c).y), float32_mul(b.x, c.sub(a).y)), float32_mul(c.x, a.sub(b).y)));\n    const aSq = toFloat(a.lengthSquared());\n    const bSq = toFloat(b.lengthSquared());\n    const cSq = toFloat(c.lengthSquared());\n\n    // Not really exact\n    const centerX =\n      toFloat(float32_add(float32_add(float32_mul(aSq, b.sub(c).y), float32_mul(bSq, c.sub(a).y)), float32_mul(cSq, a.sub(b).y)));\n    const centerY =\n      toFloat(float32_add(float32_add(float32_mul(aSq, c.sub(b).x), float32_mul(bSq, a.sub(c).x)), float32_mul(cSq, b.sub(a).x)));\n    const center = new Vec2(centerX, centerY).divide(d);\n\n    const dA = a.sub(center);\n    const dC = c.sub(center);\n\n    // Also not exact\n    const r = toFloat(dA.length());\n    const thetaStart = Math.atan2(dA.y, dA.x);\n    let thetaEnd = Math.atan2(dC.y, dC.x);\n    while (thetaEnd < thetaStart) thetaEnd += 2 * Math.PI;\n\n    let dir = 1;\n    let thetaRange = thetaEnd - thetaStart;\n    let orthoAtoC = c.sub(a);\n    orthoAtoC = new Vec2(orthoAtoC.y, -orthoAtoC.x);\n\n    if (Vec2.dot(orthoAtoC, b.sub(a)) < 0) {\n      dir = -dir;\n      thetaRange = 2 * Math.PI - thetaRange;\n    }\n    return { thetaStart, thetaRange, direction: dir, radius: r, center };\n  }\n\n  /**\n   * Creates a piecewise-linear approximation of a circular arc curve.\n   * @returns A list of vectors representing the piecewise-linear approximation.\n   */\n  static approximateCircularArc(controlPoints: Vec2[]): Vec2[] {\n    const properties = PathApproximator.circularArcProperties(controlPoints);\n    if (!properties) {\n      return PathApproximator.approximateBezier(controlPoints);\n    }\n\n    const { radius, center, thetaRange, thetaStart, direction } = properties;\n    const amountPoints =\n      2 * radius <= circularArcTolerance\n        ? 2\n        : Math.max(2, Math.ceil(thetaRange / (2 * Math.acos(1 - float32_div(circularArcTolerance, radius)))));\n\n    // We select the amount of points for the approximation by requiring the discrete curvature\n    // to be smaller than the provided tolerance. The exact angle required to meet the tolerance\n    // is: 2 * Math.Acos(1 - TOLERANCE / r)\n    // The special case is required for extremely short sliders where the radius is smaller than\n    // the tolerance. This is a pathological rather than a realistic case.\n\n    const output: Vec2[] = [];\n\n    for (let i = 0; i < amountPoints; ++i) {\n      const fract = i / (amountPoints - 1);\n      const theta = thetaStart + direction * fract * thetaRange;\n\n      const o = new Vec2(toFloat(Math.cos(theta)), toFloat(Math.sin(theta))).scale(radius);\n\n      output.push(center.add(o));\n    }\n\n    return output;\n  }\n\n  /**\n   * Creates a piecewise-linear approximation of a linear curve.\n   * Basically, returns the input.\n   * @returns A list of vectors representing the piecewise-linear approximation.\n   */\n  static approximateLinear(controlPoints: Vec2[]): Vec2[] {\n    return [...controlPoints];\n  }\n\n  /**\n   * Creates a piecewise-linear approximation of a lagrange polynomial.\n   * @returns A list of vectors representing the piecewise-linear approximation.\n   */\n  static approximateLagrangePolynomial(controlPoints: Vec2[]): Vec2[] {\n    // TODO: add some smarter logic here, chebyshev nodes?\n    const numSteps = 51;\n\n    const result: Vec2[] = [];\n\n    const weights = PathApproximator._barycentricWeights(controlPoints);\n\n    let minX = controlPoints[0].x;\n    let maxX = controlPoints[0].x;\n\n    for (let i = 1; i < controlPoints.length; i++) {\n      minX = Math.min(minX, controlPoints[i].x);\n      maxX = Math.max(maxX, controlPoints[i].x);\n    }\n\n    const dx = maxX - minX;\n\n    for (let i = 0; i < numSteps; i++) {\n      const x = minX + (dx / (numSteps - 1)) * i;\n      const y = Math.fround(PathApproximator._barycentricLagrange(controlPoints, weights, x));\n\n      result.push(new Vec2(x, y));\n    }\n\n    return result;\n  }\n\n  /**\n   * Calculates the Barycentric weights for a Lagrange polynomial for a given set of coordinates.\n   * Can be used as a helper function to compute a Lagrange polynomial repeatedly.\n   * @param points An array of coordinates. No two x should be the same.\n   */\n  static _barycentricWeights(points: Vec2[]): number[] {\n    const n = points.length;\n    const w: number[] = [];\n\n    for (let i = 0; i < n; i++) {\n      // TODO: w[i].push() -> unholey\n      w[i] = 1;\n\n      for (let j = 0; j < n; j++) {\n        if (i !== j) {\n          w[i] *= points[i].x - points[j].x;\n        }\n      }\n\n      w[i] = 1.0 / w[i];\n    }\n\n    return w;\n  }\n\n  /**\n   * Calculates the Lagrange basis polynomial for a given set of x coordinates based on previously computed barycentric\n   * weights.\n   * @param points An array of coordinates. No two x should be the same.\n   * @param weights An array of precomputed barycentric weights.\n   * @param time The x coordinate to calculate the basis polynomial for.\n   */\n  static _barycentricLagrange(points: Vec2[], weights: number[], time: number) {\n    if (points === null || points.length === 0) {\n      throw new Error(\"points must contain at least one point\");\n    }\n\n    if (points.length !== weights.length) {\n      throw new Error(\"points must contain exactly as many items as {nameof(weights)}\");\n    }\n\n    let numerator = 0;\n    let denominator = 0;\n\n    for (let i = 0, len = points.length; i < len; i++) {\n      // while this is not great with branch prediction, it prevents NaN at control point X coordinates\n      if (time === points[i].x) {\n        return points[i].y;\n      }\n\n      const li = weights[i] / (time - points[i].x);\n\n      numerator += li * points[i].y;\n      denominator += li;\n    }\n\n    return numerator / denominator;\n  }\n\n  /**\n   * Make sure the 2nd order derivative (approximated using finite elements) is within tolerable bounds.\n   * NOTE: The 2nd order derivative of a 2d curve represents its curvature, so intuitively this function\n   *       checks (as the name suggests) whether our approximation is _locally_ \"flat\". More curvy parts\n   *       need to have a denser approximation to be more \"flat\".\n   * @param controlPoints The control points to check for flatness.\n   * @returns Whether the control points are flat enough.\n   */\n  static _bezierIsFlatEnough(controlPoints: Vec2[]): boolean {\n    for (let i = 1; i < controlPoints.length - 1; i++) {\n      const tmp = controlPoints[i - 1].sub(controlPoints[i].scale(2)).add(controlPoints[i + 1]);\n\n      if (tmp.lengthSquared() > bezierTolerance ** 2 * 4) {\n        return false;\n      }\n    }\n\n    return true;\n  }\n\n  /**\n   * Subdivides n control points representing a bezier curve into 2 sets of n control points, each\n   * describing a bezier curve equivalent to a half of the original curve. Effectively this splits\n   * the original curve into 2 curves which result in the original curve when pieced back together.\n   * @param controlPoints The control points to split.\n   * @param l Output: The control points corresponding to the left half of the curve.\n   * @param r Output: The control points corresponding to the right half of the curve.\n   * @param subdivisionBuffer The first buffer containing the current subdivision state.\n   * @param count The number of control points in the original list.\n   */\n  static _bezierSubdivide(controlPoints: Vec2[], l: Vec2[], r: Vec2[], subdivisionBuffer: Vec2[], count: number): void {\n    const midpoints = subdivisionBuffer;\n\n    for (let i = 0; i < count; ++i) {\n      midpoints[i] = controlPoints[i];\n    }\n\n    for (let i = 0; i < count; ++i) {\n      l[i] = midpoints[0];\n      r[count - i - 1] = midpoints[count - i - 1];\n\n      for (let j = 0; j < count - i - 1; j++) {\n        midpoints[j] = midpoints[j].add(midpoints[j + 1]);\n        midpoints[j] = midpoints[j].divide(2);\n      }\n    }\n  }\n\n  /**\n   * This uses <a href=\"https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm De Casteljau's algorithm</a> to obtain\n   * an optimal piecewise-linear approximation of the bezier curve with the same amount of points as there are control\n   * points.\n   * @param controlPoints The control points describing the bezier curve to be approximated.\n   * @param output The points representing the resulting piecewise-linear approximation.\n   * @param count The number of control points in the original list.\n   * @param subdivisionBuffer1 The first buffer containing the current subdivision state.\n   * @param subdivisionBuffer2 The second buffer containing the current subdivision state.\n   */\n  static _bezierApproximate(\n    controlPoints: Vec2[],\n    output: Vec2[],\n    subdivisionBuffer1: Vec2[],\n    subdivisionBuffer2: Vec2[],\n    count: number,\n  ): void {\n    const l = subdivisionBuffer2;\n    const r = subdivisionBuffer1;\n\n    PathApproximator._bezierSubdivide(controlPoints, l, r, subdivisionBuffer1, count);\n\n    for (let i = 0; i < count - 1; ++i) {\n      l[count + i] = r[i + 1];\n    }\n\n    output.push(controlPoints[0]);\n\n    for (let i = 1; i < count - 1; ++i) {\n      const index = 2 * i;\n      let p = l[index - 1].add(l[index].scale(2)).add(l[index + 1]);\n      p = p.scale(Math.fround(0.25));\n\n      output.push(p);\n    }\n  }\n\n  /**\n   * Finds a point on the spline at the position of a parameter.\n   * @param vec1 The first vector.\n   * @param vec2 The second vector.\n   * @param vec3 The third vector.\n   * @param vec4 The fourth vector.\n   * @param t The parameter at which to find the point on the spline, in the range [0, 1].\n   * @returns The point on the spline at t.\n   */\n  static _catmullFindPoint(vec1: Vec2, vec2: Vec2, vec3: Vec2, vec4: Vec2, t: number): Vec2 {\n    const t2 = Math.fround(t * t);\n    const t3 = Math.fround(t * t2);\n\n    return new Vec2(\n      Math.fround(\n        0.5 *\n        (2 * vec2.x +\n          (-vec1.x + vec3.x) * t +\n          (2 * vec1.x - 5 * vec2.x + 4 * vec3.x - vec4.x) * t2 +\n          (-vec1.x + 3 * vec2.x - 3 * vec3.x + vec4.x) * t3),\n      ),\n      Math.fround(\n        0.5 *\n        (2 * vec2.y +\n          (-vec1.y + vec3.y) * t +\n          (2 * vec1.y - 5 * vec2.y + 4 * vec3.y - vec4.y) * t2 +\n          (-vec1.y + 3 * vec2.y - 3 * vec3.y + vec4.y) * t3),\n      ),\n    );\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/slider/PathControlPoint.ts",
    "content": "import { PathType } from \"./PathType\";\nimport { Position } from \"@osujs/math\";\n\nexport type PathControlPoint = {\n  // The type of path segment starting at this control point.\n  // If null, this control point will take the type of the previous part segment.\n  type?: PathType;\n  offset: Position;\n};\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/slider/PathType.ts",
    "content": "export enum PathType {\n  Catmull,\n  Bezier,\n  Linear,\n  PerfectCurve,\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/slider/SliderCheckPointDescriptor.ts",
    "content": "import { SliderCheckPointType } from \"../Types\";\n\nexport type SliderCheckPointDescriptor = {\n  type: SliderCheckPointType;\n\n  // The time when the slider ball should be tracked in order for the checkpoint to be considered \"hit\".\n  time: number;\n\n  // In a slider with repeat = 3, the spanIndex can range from 0 to 2.\n  spanIndex: number;\n\n  // The startTime of the span is useful for calculating visual stuff.\n  spanStartTime: number;\n\n  // The progress in ONE span. So first SliderRepeat would have spanProgress=1, second one would have spanProgress = 0 and so on.\n  spanProgress: number;\n};\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/slider/SliderCheckPointGenerator.ts",
    "content": "import { clamp } from \"@osujs/math\";\nimport { SliderCheckPointDescriptor } from \"./SliderCheckPointDescriptor\";\n\nfunction* generateTicks(\n  spanIndex: number,\n  spanStartTime: number,\n  spanDuration: number,\n  reversed: boolean,\n  length: number,\n  tickDistance: number,\n  minDistanceFromEnd: number,\n): IterableIterator<SliderCheckPointDescriptor> {\n  for (let d = tickDistance; d <= length; d += tickDistance) {\n    if (d >= length - minDistanceFromEnd) {\n      break;\n    }\n    const spanProgress = d / length;\n    const timeProgress = reversed ? 1.0 - spanProgress : spanProgress;\n    yield {\n      type: \"TICK\",\n      spanIndex,\n      spanStartTime,\n      time: spanStartTime + timeProgress * spanDuration,\n      spanProgress,\n    };\n  }\n}\n\nexport function* generateSliderCheckpoints(\n  startTime: number,\n  spanDuration: number,\n  velocity: number,\n  tickDistance: number,\n  totalDistance: number,\n  spanCount: number,\n  legacyLastTickOffset?: number,\n): IterableIterator<SliderCheckPointDescriptor> {\n  const length = Math.min(100000.0, totalDistance);\n  tickDistance = clamp(tickDistance, 0.0, length);\n  const minDistanceFromEnd = velocity * 10;\n\n  // Generating ticks, repeats\n  // Using `floatEqual` was my suggestion, but osu!lazer uses tickDistance != 0\n  if (tickDistance !== 0) {\n    for (let span = 0; span < spanCount; span++) {\n      const spanStartTime = startTime + span * spanDuration;\n      const reversed: boolean = span % 2 === 1;\n\n      const it = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd);\n\n      // Don't flame me for this\n      const ticks: SliderCheckPointDescriptor[] = [];\n      for (const t of it) ticks.push(t);\n      if (reversed) ticks.reverse();\n      for (const t of ticks) yield t;\n\n      if (span < spanCount - 1) {\n        yield {\n          type: \"REPEAT\",\n          spanIndex: span,\n          spanStartTime,\n          time: spanStartTime + spanDuration,\n          spanProgress: (span + 1) % 2,\n        };\n      }\n    }\n  }\n\n  const totalDuration = spanCount * spanDuration;\n  const finalSpanIndex = spanCount - 1;\n  const finalSpanStartTime = startTime + finalSpanIndex * spanDuration;\n  const finalSpanEndTime = Math.max(\n    startTime + totalDuration / 2.0,\n    finalSpanStartTime + spanDuration - (legacyLastTickOffset ?? 0),\n  );\n  let finalProgress = (finalSpanEndTime - finalSpanStartTime) / spanDuration;\n  if (spanCount % 2 === 0) finalProgress = 1.0 - finalProgress;\n  yield {\n    type: \"LAST_LEGACY_TICK\",\n    spanIndex: finalSpanIndex,\n    spanStartTime: finalSpanStartTime,\n    time: finalSpanEndTime,\n    spanProgress: finalProgress,\n  };\n\n  // Technically speaking the tail has no real relevancy for gameplay, it is just a visual element.\n  // In Slider.cs it is even ignored...\n\n  // yield {\n  //   type: SliderCheckPointType.TAIL,\n  //   spanIndex: finalSpanIndex,\n  //   spanStartTime: startTime + (spanCount - 1) * spanDuration,\n  //   time: startTime + totalDuration,\n  //   pathProgress: spanCount % 2\n  // };\n}\n"
  },
  {
    "path": "libs/osu/core/src/hitobjects/slider/SliderPath.ts",
    "content": "import { PathControlPoint } from \"./PathControlPoint\";\nimport { PathType } from \"./PathType\";\nimport { clamp, doubleEqual, Position, Vec2 } from \"@osujs/math\";\nimport { PathApproximator } from \"./PathApproximator\";\n\nfunction mapToVector2(p: Position[]) {\n  return p.map((p) => new Vec2(p.x, p.y));\n}\n\nexport class SliderPath {\n  controlPoints: PathControlPoint[];\n\n  // Cached helper data that should only be calculated when there is change in the control points.\n  private _invalid: boolean;\n  private _cumulativeLength: number[];\n  private _calculatedPath: Position[];\n  private _min: Position = { x: 0, y: 0 };\n  private _max: Position = { x: 0, y: 0 };\n  private readonly _expectedDistance?: number;\n\n  constructor(controlPoints: PathControlPoint[], length?: number) {\n    this.controlPoints = controlPoints;\n    this._invalid = true;\n    this._cumulativeLength = [];\n    this._calculatedPath = [];\n    this._expectedDistance = length;\n  }\n\n  get cumulativeLengths(): number[] {\n    this.ensureValid();\n    return this._cumulativeLength;\n  }\n\n  get calculatedPath(): Position[] {\n    this.ensureValid();\n    return this._calculatedPath;\n  }\n\n  makeInvalid(): void {\n    this._invalid = true;\n  }\n\n  // Recalculates the helper data if needed\n  ensureValid(): void {\n    if (this._invalid) {\n      this.calculatePath();\n      this.calculateLength();\n      this.calculateBoundaryBox();\n      this._invalid = false;\n    }\n  }\n\n  calculateSubPath(subControlPoints: Position[], type: PathType): Position[] {\n    switch (type) {\n      case PathType.Catmull:\n        return PathApproximator.approximateCatmull(mapToVector2(subControlPoints));\n      case PathType.Linear:\n        return PathApproximator.approximateLinear(mapToVector2(subControlPoints));\n      case PathType.PerfectCurve:\n        if (subControlPoints.length !== 3) break;\n\n        // eslint-disable-next-line no-case-declarations\n        const subpath = PathApproximator.approximateCircularArc(mapToVector2(subControlPoints));\n        // If for some reason a circular arc could not be fit to the 3 given points, fall back to a numerically stable\n        // bezier approximation.\n        if (subpath.length === 0) break;\n        return subpath;\n    }\n    return PathApproximator.approximateBezier(mapToVector2(subControlPoints));\n  }\n\n  get boundaryBox(): [Position, Position] {\n    this.ensureValid();\n    return [this._min, this._max];\n  }\n\n  calculateBoundaryBox(): [Position, Position] {\n    // Since it is osu!px , it should be no problem\n    let minX = 3000,\n      maxX = -3000,\n      minY = 3000,\n      maxY = -3000;\n    this._calculatedPath.forEach((p) => {\n      minX = Math.min(minX, p.x);\n      maxX = Math.max(maxX, p.x);\n      minY = Math.min(minY, p.y);\n      maxY = Math.max(maxY, p.y);\n    });\n    this._min = new Vec2(minX, minY);\n    this._max = new Vec2(maxX, maxY);\n    return [this._min, this._max];\n  }\n\n  calculatePath(): void {\n    this._calculatedPath = [];\n    const numberOfPoints = this.controlPoints.length;\n    if (numberOfPoints === 0) return;\n\n    const vertices = this.controlPoints.map((p) => p.offset);\n    let start = 0;\n    for (let i = 0; i < numberOfPoints; i++) {\n      // Need to calculate previous segment\n      if (this.controlPoints[i].type === undefined && i < numberOfPoints - 1) {\n        continue;\n      }\n\n      // Current vertex ends the segment\n      const segmentVertices = vertices.slice(start, i + 1);\n      const segmentType = this.controlPoints[start].type ?? PathType.Linear;\n      for (const t of this.calculateSubPath(segmentVertices, segmentType)) {\n        const n = this._calculatedPath.length;\n        if (n === 0 || !Vec2.equal(this._calculatedPath[n - 1], t)) {\n          this._calculatedPath.push(t);\n        }\n      }\n      start = i;\n    }\n  }\n\n  calculateLength(): void {\n    this._cumulativeLength = new Array<number>(this._calculatedPath.length);\n    this._cumulativeLength[0] = 0.0;\n    for (let i = 1; i < this._calculatedPath.length; i++) {\n      this._cumulativeLength[i] =\n        this._cumulativeLength[i - 1] + Math.fround(Vec2.distance(this._calculatedPath[i - 1], this._calculatedPath[i]));\n    }\n    const calculatedLength = this._cumulativeLength[this._cumulativeLength.length - 1];\n\n    // TODO: In lazer the != operator is used, but shouldn't the approximate equal be used?\n    if (this._expectedDistance !== undefined && calculatedLength !== this._expectedDistance) {\n      // In osu-stable, if the last two control points of a slider are equal, extension is not performed.\n      if (this.controlPoints.length >= 2 && Vec2.equal(this.controlPoints[this.controlPoints.length - 1].offset, this.controlPoints[this.controlPoints.length - 2].offset)\n        && this._expectedDistance > calculatedLength) {\n        this._cumulativeLength.push(calculatedLength);\n        return;\n      }\n      // The last length is always incorrect\n      this._cumulativeLength.splice(this._cumulativeLength.length - 1);\n\n      let pathEndIndex = this._calculatedPath.length - 1;\n      if (calculatedLength > this._expectedDistance) {\n        while (\n          this._cumulativeLength.length > 0 &&\n          this._cumulativeLength[this._cumulativeLength.length - 1] >= this._expectedDistance\n          ) {\n          this._cumulativeLength.splice(this._cumulativeLength.length - 1);\n          this._calculatedPath.splice(pathEndIndex--, 1);\n        }\n      }\n      if (pathEndIndex <= 0) {\n        // TODO: Perhaps negative path lengths should be disallowed together\n        this._cumulativeLength.push(0);\n        return;\n      }\n      // the direction of the segment to shorten or lengthen\n      const dir = Vec2.sub(this._calculatedPath[pathEndIndex], this._calculatedPath[pathEndIndex - 1]).normalized();\n\n      const f = this._expectedDistance - this._cumulativeLength[this._cumulativeLength.length - 1];\n      this._calculatedPath[pathEndIndex] = Vec2.add(this._calculatedPath[pathEndIndex - 1], dir.scale(Math.fround(f)));\n      this._cumulativeLength.push(this._expectedDistance);\n    }\n  }\n\n  get distance(): number {\n    const cumulativeLengths = this.cumulativeLengths;\n    const count = cumulativeLengths.length;\n    return count > 0 ? cumulativeLengths[count - 1] : 0.0;\n  }\n\n  private indexOfDistance(distance: number): number {\n    // TODO: Binary search the first value that is not less than partialDistance\n    const idx = this.cumulativeLengths.findIndex((value) => value >= distance);\n    if (idx === undefined) {\n      // Should not be possible\n      throw new Error(\"Cumulative lengths or distance wrongly programmed\");\n    } else {\n      return idx;\n    }\n  }\n\n  /**\n   * Calculates the position of the slider at the given progress.\n   * @param progress a number between 0 (head) and 1 (tail/repeat)\n   */\n  positionAt(progress: number): Position {\n    const partialDistance = this.distance * clamp(progress, 0, 1);\n    return this.interpolateVertices(this.indexOfDistance(partialDistance), partialDistance);\n  }\n\n  // d: double\n  private interpolateVertices(i: number, d: number): Position {\n    const calculatedPath = this.calculatedPath;\n    if (calculatedPath.length === 0) return Vec2.Zero;\n    if (i <= 0) return calculatedPath[0];\n    if (i >= calculatedPath.length) return calculatedPath[calculatedPath.length - 1];\n    const p1 = calculatedPath[i - 1];\n    const p2 = calculatedPath[i];\n    const d1 = this.cumulativeLengths[i - 1];\n    const d2 = this.cumulativeLengths[i];\n    if (doubleEqual(d1, d2)) {\n      return p1;\n    }\n    // Number between 0 and 1\n    const z = (d - d1) / (d2 - d1);\n    return Vec2.interpolate(p1, p2, z);\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/index.ts",
    "content": "// audio\nexport * from \"./audio/HitSampleInfo\";\n\n// beatmap\nexport * from \"./beatmap/ControlPoints/ControlPoint\";\nexport * from \"./beatmap/ControlPoints/ControlPointGroup\";\nexport * from \"./beatmap/ControlPoints/DifficultyControlPoint\";\nexport * from \"./beatmap/ControlPoints/TimingControlPoint\";\nexport * from \"./mods/HardRockMod\";\nexport * from \"./mods/HiddenMod\";\nexport * from \"./mods/Mods\";\nexport * from \"./mods/StackingMod\";\nexport * from \"./beatmap/BeatmapBuilder\";\nexport * from \"./beatmap/BeatmapDifficulty\";\nexport * from \"./beatmap/Beatmap\";\n\n// blueprints\nexport * from \"./blueprint/Blueprint\";\nexport * from \"./blueprint/HitObjectSettings\";\nexport { parseBlueprint } from \"./blueprint/BlueprintParser\";\nexport type { BlueprintSection } from \"./blueprint/BlueprintParser\";\n\n// hitobjects\n\nexport * from \"./hitobjects/slider/PathApproximator\";\nexport * from \"./hitobjects/slider/PathControlPoint\";\nexport * from \"./hitobjects/slider/PathType\";\nexport * from \"./hitobjects/slider/SliderPath\";\nexport * from \"./hitobjects/HitCircle\";\nexport * from \"./hitobjects/Types\";\nexport * from \"./hitobjects/Slider\";\nexport * from \"./hitobjects/SliderCheckPoint\";\nexport * from \"./hitobjects/Spinner\";\n\n// gameplay\n\nexport * from \"./gameplay/GameplayAnalysisEvent\";\nexport * from \"./gameplay/GameplayInfo\";\nexport * from \"./gameplay/GameState\";\nexport * from \"./gameplay/GameStateEvaluator\";\nexport * from \"./gameplay/GameStateTimeMachine\";\nexport * from \"./gameplay/Verdicts\";\n\n// replays\nexport * from \"./replays/RawReplayData\";\nexport * from \"./replays/Replay\";\nexport * from \"./replays/ReplayClicks\";\nexport { parseReplayFramesFromRaw } from \"./replays/ReplayParser\";\n\nexport * from \"./playfield\";\n\nexport * from \"./utils/index\";\n"
  },
  {
    "path": "libs/osu/core/src/mods/EasyMod.ts",
    "content": "import { BeatmapDifficulty, BeatmapDifficultyAdjuster } from \"../\";\n\nconst ratio = 0.5;\n\nexport class EasyMod {\n  static difficultyAdjuster: BeatmapDifficultyAdjuster = (base: BeatmapDifficulty): BeatmapDifficulty => ({\n    ...base, // SliderDiffs\n    overallDifficulty: base.overallDifficulty * ratio,\n    approachRate: base.approachRate * ratio,\n    drainRate: base.drainRate * ratio,\n    circleSize: base.circleSize * ratio,\n  });\n}\n"
  },
  {
    "path": "libs/osu/core/src/mods/HardRockMod.ts",
    "content": "import { BeatmapDifficulty } from \"../beatmap/BeatmapDifficulty\";\nimport { BeatmapDifficultyAdjuster } from \"./Mods\";\nimport { Position } from \"@osujs/math\";\nimport { isHitCircle, isSlider, OsuHitObject } from \"../hitobjects/Types\";\nimport { OSU_PLAYFIELD_HEIGHT } from \"../playfield\";\n\nfunction flipY(position: Position) {\n  const { x, y } = position;\n  return { x, y: OSU_PLAYFIELD_HEIGHT - y };\n}\n\nexport class HardRockMod {\n  static difficultyAdjuster: BeatmapDifficultyAdjuster = (base: BeatmapDifficulty): BeatmapDifficulty => ({\n    ...base, // SliderDiffs\n    overallDifficulty: Math.min(10, base.overallDifficulty * 1.4),\n    approachRate: Math.min(10, base.approachRate * 1.4),\n    drainRate: Math.min(10, base.drainRate * 1.4),\n    circleSize: Math.min(10, base.circleSize * 1.3),\n  });\n\n  static flipVertically = (hitObjects: OsuHitObject[]) => {\n    hitObjects.forEach((h) => {\n      if (isHitCircle(h)) {\n        h.position = flipY(h.position);\n        h.unstackedPosition = flipY(h.unstackedPosition);\n      } else if (isSlider(h)) {\n        // TODO: Need to set invalid as well or just recreate the checkpoints from control points\n        h.head.position = flipY(h.head.position);\n        h.head.unstackedPosition = flipY(h.head.unstackedPosition);\n        h.path.controlPoints.forEach((p) => {\n          p.offset.y *= -1;\n        });\n        h.path.makeInvalid();\n        h.checkPoints.forEach((p) => {\n          p.offset.y *= -1;\n        });\n      }\n    });\n  };\n}\n"
  },
  {
    "path": "libs/osu/core/src/mods/HiddenMod.ts",
    "content": "export const ModHiddenConstants = {\n  FADE_IN_DURATION_MULTIPLIER: 0.4,\n  FADE_OUT_DURATION_MULTIPLIER: 0.3,\n};\n"
  },
  {
    "path": "libs/osu/core/src/mods/Mods.spec.ts",
    "content": "import { DEFAULT_BEATMAP_DIFFICULTY } from \"../beatmap/BeatmapDifficulty\";\nimport { HardRockMod } from \"./HardRockMod\";\nimport { EasyMod } from \"./EasyMod\";\n\ndescribe(\"HardRock\", function () {\n  describe(\"BeatmapDifficulty adjusting\", function () {\n    it(\"should adjust to 10 max\", function () {\n      const original = {\n        ...DEFAULT_BEATMAP_DIFFICULTY,\n        approachRate: 8,\n        drainRate: 9,\n        overallDifficulty: 8,\n        circleSize: 4,\n      };\n      const expected = {\n        ...DEFAULT_BEATMAP_DIFFICULTY,\n        approachRate: 10,\n        drainRate: 10,\n        overallDifficulty: 10,\n        circleSize: 5.2,\n      };\n      const actual = HardRockMod.difficultyAdjuster(original);\n      expect(actual).toEqual(expected);\n    });\n  });\n});\n\ntest(\"EasyMod should half AR, OD, CS, HP \", () => {\n  const original = {\n    ...DEFAULT_BEATMAP_DIFFICULTY,\n    approachRate: 8,\n    drainRate: 9,\n    overallDifficulty: 8,\n    circleSize: 4,\n  };\n  const expected = {\n    ...DEFAULT_BEATMAP_DIFFICULTY,\n    approachRate: 4,\n    drainRate: 4.5,\n    overallDifficulty: 4,\n    circleSize: 2,\n  };\n  const actual = EasyMod.difficultyAdjuster(original);\n  expect(actual).toEqual(expected);\n\n  // Test immutability\n  expect(original.approachRate).toEqual(8);\n});\n"
  },
  {
    "path": "libs/osu/core/src/mods/Mods.ts",
    "content": "import { BeatmapDifficulty } from \"../beatmap/BeatmapDifficulty\";\nimport { HardRockMod } from \"./HardRockMod\";\nimport { EasyMod } from \"./EasyMod\";\n\nexport type BeatmapDifficultyAdjuster = (d: BeatmapDifficulty) => BeatmapDifficulty;\n\n// https://osu.ppy.sh/wiki/en/Game_modifier\n\nexport const OsuClassicMods = [\n  \"EASY\",\n  \"HALF_TIME\",\n  \"NO_FAIL\",\n  \"HARD_ROCK\",\n  \"SUDDEN_DEATH\",\n  \"PERFECT\",\n  \"DOUBLE_TIME\",\n  \"NIGHT_CORE\",\n  \"HIDDEN\",\n  \"FLASH_LIGHT\",\n  \"AUTO_PLAY\",\n  \"AUTO_PILOT\",\n  \"RELAX\",\n  \"SPUN_OUT\",\n  \"SCORE_V2\",\n] as const;\n\nexport type OsuClassicMod = typeof OsuClassicMods[number];\n\ninterface ModSetting {\n  name: string;\n  scoreMultiplier?: number;\n  difficultyAdjuster?: BeatmapDifficultyAdjuster;\n}\n\nexport const ModSettings: Record<OsuClassicMod, ModSetting> = {\n  EASY: {\n    name: \"Easy\",\n    difficultyAdjuster: EasyMod.difficultyAdjuster,\n    scoreMultiplier: 0.5,\n  },\n  HARD_ROCK: {\n    name: \"Hard Rock\",\n    scoreMultiplier: 1.06,\n    difficultyAdjuster: HardRockMod.difficultyAdjuster,\n  },\n  DOUBLE_TIME: { name: \"Double Time\", scoreMultiplier: 1.12 },\n  FLASH_LIGHT: { name: \"Flash Light\", scoreMultiplier: 1.12 },\n  HALF_TIME: { name: \"Half Time\", scoreMultiplier: 0.3 },\n  HIDDEN: { name: \"Hidden\", scoreMultiplier: 1.06 },\n  NIGHT_CORE: { name: \"Night Core\", scoreMultiplier: 1.12 },\n  NO_FAIL: { name: \"No Fail\", scoreMultiplier: 0.5 },\n  AUTO_PLAY: { name: \"Auto Play\" },\n  AUTO_PILOT: { name: \"Auto Pilot\" },\n  PERFECT: { name: \"Perfect\" },\n  RELAX: { name: \"Relax\" },\n  SCORE_V2: { name: \"Score V2\" },\n  SPUN_OUT: { name: \"Spun Out\" },\n  SUDDEN_DEATH: { name: \"Sudden Death\" },\n};\n\n// How early before a hitobject's time Relax can hit. (in ms)\nexport const RELAX_LENIENCY = 12;\n"
  },
  {
    "path": "libs/osu/core/src/mods/StackingMod.ts",
    "content": "import { HitCircle } from \"../hitobjects/HitCircle\";\nimport { float32_mul, Position, Vec2 } from \"@osujs/math\";\nimport { Slider } from \"../hitobjects/Slider\";\nimport { isHitCircle, isSlider, isSpinner, OsuHitObject } from \"../hitobjects/Types\";\n\nfunction stackOffset(stackHeight: number, scale: number): Position {\n  const value = float32_mul(stackHeight, float32_mul(scale, -6.4));\n  return { x: value, y: value };\n}\n\nfunction stackedPosition(initialPosition: Position, stackHeight: number, scale: number): Position {\n  const offset = stackOffset(stackHeight, scale);\n  return Vec2.add(initialPosition, offset);\n}\n\ntype StackableHitObject = Slider | HitCircle;\nconst STACK_DISTANCE = 3;\n\n// I refuse to put an endPosition and endTime into HitCircle just because it's then easier to code it here\n// How does it even make sense that an HitCircle has an \"endPosition\" or \"endTime\".\n// Or how does it make sense that a Spinner has a stacking position, when it even doesn't have a position?\n\nconst hitCircle = (o: StackableHitObject) => (isSlider(o) ? o.head : o);\nconst approachDuration = (o: StackableHitObject) => hitCircle(o).approachDuration;\nconst hitTime = (o: StackableHitObject) => hitCircle(o).hitTime;\nconst position = (o: StackableHitObject) => hitCircle(o).position;\nconst endPosition = (o: StackableHitObject) => (isSlider(o) ? o.endPosition : o.position);\nconst endTime = (o: StackableHitObject) => (isSlider(o) ? o.endTime : o.hitTime);\n\nfunction createStackingHeights(hitObjects: OsuHitObject[]) {\n  const stackingHeights = new Map<string, number>();\n\n  function setH(o: StackableHitObject, val: number) {\n    stackingHeights.set(hitCircle(o).id, val);\n  }\n\n  function H(o: StackableHitObject) {\n    return stackingHeights.get(hitCircle(o).id) ?? 0;\n  }\n\n  // They all have 0 as stack heights\n  for (const ho of hitObjects) {\n    if (!isSpinner(ho)) {\n      setH(ho, 0);\n    }\n  }\n  return { stackingHeights, setH, H };\n}\n\nfunction newStackingHeights(hitObjects: OsuHitObject[], stackLeniency: number): Map<string, number> {\n  const startIndex = 0;\n  const endIndex = hitObjects.length - 1;\n  const extendedEndIndex = endIndex;\n  const { stackingHeights, setH, H } = createStackingHeights(hitObjects);\n\n  // Reverse pass for stack calculation\n  let extendedStartIndex = startIndex;\n\n  for (let i = extendedEndIndex; i > startIndex; i--) {\n    let n = i;\n\n    let objectI = hitObjects[i];\n    if (isSpinner(objectI) || H(objectI) !== 0) continue;\n\n    const stackThreshold = approachDuration(objectI) * stackLeniency;\n\n    if (isHitCircle(objectI)) {\n      while (--n >= 0) {\n        const objectN = hitObjects[n];\n        if (isSpinner(objectN)) break;\n\n        if (hitTime(objectI) - endTime(objectN) > stackThreshold) break;\n        if (n < extendedStartIndex) {\n          setH(objectN, 0);\n          extendedStartIndex = n;\n        }\n\n        if (isSlider(objectN) && Vec2.distance(endPosition(objectN), position(objectI)) < STACK_DISTANCE) {\n          const offset = H(objectI) - H(objectN) + 1;\n          for (let j = n + 1; j <= i; j++) {\n            const objectJ = hitObjects[j];\n            if (isSpinner(objectJ)) continue; // TODO: Inserted, but not sure\n            if (Vec2.distance(endPosition(objectN), position(objectJ)) < STACK_DISTANCE) {\n              setH(objectJ, H(objectJ) - offset);\n            }\n          }\n          break;\n        }\n        if (Vec2.distance(position(objectN), position(objectI)) < STACK_DISTANCE) {\n          setH(objectN, H(objectI) + 1);\n          objectI = objectN;\n        }\n      }\n    } else {\n      while (--n >= startIndex) {\n        const objectN = hitObjects[n];\n        if (isSpinner(objectN)) continue;\n        if (hitTime(objectI) - hitTime(objectN) > stackThreshold) break;\n\n        if (Vec2.distance(endPosition(objectN), position(objectI)) < STACK_DISTANCE) {\n          setH(objectN, H(objectI) + 1);\n          objectI = objectN;\n        }\n      }\n    }\n  }\n  return stackingHeights;\n}\n\nfunction oldStackingHeights(hitObjects: OsuHitObject[], stackLeniency: number) {\n  const { stackingHeights, H, setH } = createStackingHeights(hitObjects);\n  for (let i = 0; i < hitObjects.length; i++) {\n    const currHitObject = hitObjects[i];\n    if (isSpinner(currHitObject)) continue;\n    if (H(currHitObject) !== 0 && !isSlider(currHitObject)) {\n      continue;\n    }\n\n    let startTime = endTime(currHitObject);\n    let sliderStack = 0;\n\n    for (let j = i + 1; j < hitObjects.length; j++) {\n      const stackThreshold = approachDuration(currHitObject) * stackLeniency;\n      const nextHitObject = hitObjects[j];\n\n      if (isSpinner(nextHitObject)) continue;\n      if (hitTime(nextHitObject) - stackThreshold > startTime) {\n        break;\n      }\n\n      const position2 = isSlider(currHitObject) ? currHitObject.endPosition : currHitObject.position;\n      if (Vec2.withinDistance(position(nextHitObject), position(currHitObject), STACK_DISTANCE)) {\n        setH(currHitObject, H(currHitObject) + 1);\n        startTime = endTime(nextHitObject);\n      } else if (Vec2.withinDistance(position(nextHitObject), position2, STACK_DISTANCE)) {\n        sliderStack++;\n        setH(nextHitObject, H(nextHitObject) - sliderStack);\n        startTime = endTime(nextHitObject);\n      }\n    }\n  }\n  return stackingHeights;\n}\n\n// Modifies the hitObjects according to the stacking algorithm.\nexport function modifyStackingPosition(hitObjects: OsuHitObject[], stackLeniency: number, beatmapVersion: number) {\n  const heights = (() => {\n    if (beatmapVersion >= 6) {\n      return newStackingHeights(hitObjects, stackLeniency);\n    } else {\n      return oldStackingHeights(hitObjects, stackLeniency);\n    }\n  })();\n  hitObjects.forEach((hitObject) => {\n    if (isSpinner(hitObject as OsuHitObject)) return;\n    const h = hitCircle(hitObject as StackableHitObject);\n    const height = heights.get(h.id);\n    if (height === undefined) {\n      throw Error(\"Stack height can't be undefined\");\n    }\n    h.position = stackedPosition(h.position, height, h.scale);\n  });\n}\n"
  },
  {
    "path": "libs/osu/core/src/playfield.ts",
    "content": "export const OSU_PLAYFIELD_HEIGHT = 384;\nexport const OSU_PLAYFIELD_WIDTH = 512;\n"
  },
  {
    "path": "libs/osu/core/src/replays/RawReplayData.ts",
    "content": "/**\n * The one that gets parsed from an .osr file\n * This is exactly like the one you would get from osr-node\n */\nimport { OsuClassicMod, OsuClassicMods } from \"../mods/Mods\";\n\nexport class RawReplayData {\n  gameMode = 0;\n  gameVersion = 0;\n  beatmapMD5 = \"\";\n  playerName = \"\";\n  replayMD5 = \"\";\n  number_300s = 0;\n  number_100s = 0;\n  number_50s = 0;\n  gekis = 0;\n  katus = 0;\n  misses = 0;\n  score = 0;\n  max_combo = 0;\n  perfect_combo = 0;\n  mods = 0;\n  life_bar = \"\";\n  timestamp = 0;\n  replay_length = 0;\n  replay_data = \"\";\n  unknown = 0;\n}\n\n// https://github.com/ppy/osu/blob/7654df94f6f37b8382be7dfcb4f674e03bd35427/osu.Game/Beatmaps/Legacy/LegacyMods.cs\nexport const ReplayModBit: Record<OsuClassicMod, number> = {\n  NO_FAIL: 1 << 0,\n  EASY: 1 << 1,\n  // \"TOUCH_DEVICE\": 1 << 2,\n  HIDDEN: 1 << 3,\n  HARD_ROCK: 1 << 4,\n  SUDDEN_DEATH: 1 << 5,\n  DOUBLE_TIME: 1 << 6,\n  RELAX: 1 << 7,\n  HALF_TIME: 1 << 8,\n  NIGHT_CORE: 1 << 9,\n  FLASH_LIGHT: 1 << 10,\n  AUTO_PLAY: 1 << 11,\n  SPUN_OUT: 1 << 12,\n  AUTO_PILOT: 1 << 13,\n  PERFECT: 1 << 14,\n  SCORE_V2: 1 << 29,\n};\n\nexport function modsFromBitmask(modMask: number): OsuClassicMod[] {\n  const list: OsuClassicMod[] = [];\n  for (const mod of OsuClassicMods) {\n    const bit = ReplayModBit[mod];\n    if ((modMask & bit) > 0) {\n      list.push(mod);\n    }\n  }\n  return list;\n}\n\nexport function modsToBitmask(mods: OsuClassicMod[]): number {\n  let mask = 0;\n  for (const mod of mods) {\n    mask |= 1 << ReplayModBit[mod];\n  }\n  return mask;\n}\n"
  },
  {
    "path": "libs/osu/core/src/replays/Replay.ts",
    "content": "import { Position } from \"@osujs/math\";\n\nexport enum OsuAction {\n  leftButton,\n  rightButton,\n}\n\nexport type ReplayFrame = {\n  time: number;\n  position: Position;\n  actions: OsuAction[];\n};\n\nexport enum ReplayButtonState {\n  None = 0,\n  Left1 = 1,\n  Right1 = 2,\n  Left2 = 4,\n  Right2 = 8,\n  Smoke = 16,\n}\n"
  },
  {
    "path": "libs/osu/core/src/replays/ReplayClicks.ts",
    "content": "import { ReplayFrame } from \"./Replay\";\n\nexport type TimeInterval = [number, number];\nexport type TimeIntervals = TimeInterval[];\n\nexport function calculateReplayClicks(frames: ReplayFrame[]) {\n  const clicks: [TimeIntervals, TimeIntervals] = [[], []];\n  const startTime: (number | null)[] = [null, null];\n  for (const frame of frames) {\n    for (let i = 0; i < 2; i++) {\n      // Enums are so bad in terms of type safety\n      const isPressing = frame.actions.includes(i);\n      if (!isPressing && startTime[i] !== null) {\n        clicks[i].push([startTime[i] as number, frame.time]);\n        startTime[i] = null;\n      } else if (isPressing && startTime[i] === null) {\n        startTime[i] = frame.time;\n      }\n    }\n  }\n  for (let i = 0; i < 2; i++) {\n    if (startTime[i] !== null) {\n      clicks[i].push([startTime[i] as number, 1e9]);\n    }\n  }\n  return clicks;\n}\n"
  },
  {
    "path": "libs/osu/core/src/replays/ReplayParser.spec.ts",
    "content": "import { OsuAction, ReplayFrame } from \"./Replay\";\nimport { parseReplayFramesFromRaw } from \"./ReplayParser\";\n\n// w, x, y, z\n// w time since last action\n// x, y coordinates\n// z bitmask of what was pressed\ndescribe(\"Parsing rawReplayData (from node-osr)\", function () {\n  it(\"should ignore the first three frames from legacy due to negative\", function () {\n    // From RyuK +HDDT Akatsuki Zukuyo replay\n    const raw = \"0|256|-500|0,-1|256|-500|0,-1171|257.0417|124.7764|1\";\n    const actual = parseReplayFramesFromRaw(raw);\n    expect(actual).toStrictEqual([]);\n  });\n  it(\"should have the first correct frame\", function () {\n    // From RyuK +HDDT Akatsuki Zukuyo replay\n    const raw = \"0|256|-500|0,-1|256|-500|0,-1171|257.0417|124.7764|1,13|256.8854|124.8789|1\";\n    const f1: ReplayFrame = {\n      time: -1171 - 1 + 13,\n      position: { x: 256.8854, y: 124.8789 },\n      actions: [OsuAction.leftButton],\n    };\n    const actual = parseReplayFramesFromRaw(raw);\n    expect(actual).toEqual([f1]);\n  });\n});\n"
  },
  {
    "path": "libs/osu/core/src/replays/ReplayParser.ts",
    "content": "import { OsuAction, ReplayButtonState, ReplayFrame } from \"./Replay\";\n\nconst MAX_COORDINATE_VALUE = 131072;\n// LegacyScoreDecoder.cs\n// PowerOfTwo bit\nconst bitmaskCheck = (mask: number, bit: number): boolean => (mask & bit) !== 0;\nexport const parseReplayFramesFromRaw = (rawString: string): ReplayFrame[] => {\n  const frameStrings = rawString.split(\",\");\n  let lastTime = 0;\n  const frames: ReplayFrame[] = [];\n  for (let i = 0; i < frameStrings.length; i++) {\n    const split = frameStrings[i].split(\"|\");\n    if (split.length < 4) continue;\n\n    if (split[0] === \"-12345\") {\n      // osu-lazer-comment: The seed is provided in split[3], which we'll need to use at some point\n      continue;\n    }\n\n    const diff = parseFloat(split[0]);\n    const mouseX = parseFloat(split[1]);\n    const mouseY = parseFloat(split[2]);\n    if (Math.abs(mouseX) > MAX_COORDINATE_VALUE || Math.abs(mouseY) > MAX_COORDINATE_VALUE) {\n      throw Error(\"Value overflow while parsing mouse coordinates\");\n    }\n    lastTime += diff;\n    if (i < 2 && mouseX === 256 && mouseY === -500)\n      // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively.\n      // both frames use a position of (256, -500).\n      // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania)\n      continue;\n\n    // osu-lazer-comment: At some point we probably want to rewind and play back the negative-time frames\n    // but for now we'll achieve equal playback to stable by skipping negative frames\n\n    if (diff < 0) continue;\n    const actions: OsuAction[] = [];\n    const b = parseInt(split[3]);\n\n    if (bitmaskCheck(b, ReplayButtonState.Left1) || bitmaskCheck(b, ReplayButtonState.Left2))\n      actions.push(OsuAction.leftButton);\n    if (bitmaskCheck(b, ReplayButtonState.Right1) || bitmaskCheck(b, ReplayButtonState.Right2))\n      actions.push(OsuAction.rightButton);\n\n    frames.push({ actions, position: { x: mouseX, y: mouseY }, time: lastTime });\n  }\n\n  // We do the following merging because some frames have the same time, but the actions have to be merged together.\n\n  const mergedFrames: ReplayFrame[] = [];\n  let last;\n  for (let i = 0; i < frames.length; i++) {\n    if (last === undefined || frames[i].time !== last.time) {\n      mergedFrames.push(frames[i]);\n      last = frames[i];\n    } else {\n      for (let j = 0; j < frames[i].actions.length; j++) {\n        const a = frames[i].actions[j];\n        if (!last.actions.includes(a)) {\n          last.actions.push(a);\n        }\n      }\n      last.actions.sort();\n    }\n  }\n\n  return mergedFrames;\n};\n"
  },
  {
    "path": "libs/osu/core/src/utils/SortedList.spec.ts",
    "content": "import { Comparable, SortedList } from \"./SortedList\";\n\ndescribe(\"SortedList with a simple Comparable class\", function () {\n  class A implements Comparable<A> {\n    value: number;\n\n    constructor(value: number) {\n      this.value = value;\n    }\n\n    compareTo(t: A): number {\n      return this.value - t.value;\n    }\n  }\n\n  it(\"should be sorted after adding two elements in the sorted order\", function () {\n    const list = new SortedList<A>();\n    list.add(new A(309));\n    list.add(new A(400));\n    expect(list.get(0).value).toBe(309);\n    expect(list.get(1).value).toBe(400);\n  });\n\n  it(\"should be sorted after adding two elements in non sorted order\", function () {\n    const list = new SortedList<A>();\n    list.add(new A(400));\n    list.add(new A(309));\n    expect(list.get(0).value).toBe(309);\n    expect(list.get(1).value).toBe(400);\n  });\n});\n"
  },
  {
    "path": "libs/osu/core/src/utils/SortedList.ts",
    "content": "import { floatEqual } from \"@osujs/math\";\n\nexport interface Comparable<T> {\n  compareTo(t: T): number;\n}\n\nexport class SortedList<T extends Comparable<T>> {\n  list: T[];\n\n  constructor() {\n    this.list = [];\n  }\n\n  // binary search or not -> the insert/remove is also O(n) ....\n  indexOf(t: T): number {\n    return this.list.findIndex((value) => floatEqual(t.compareTo(value), 0));\n  }\n\n  // This will also maintain insertion order, which means that adding a 2' to [1, 2, 3] will result to [1, 2, 2', 3].\n  add(t: T): void {\n    const i = this.list.findIndex((value) => t.compareTo(value) < 0);\n    if (i === -1) {\n      // This means that there is no element that is larger than the given value\n      this.list.splice(this.list.length, 0, t);\n    } else {\n      this.list.splice(i, 0, t);\n    }\n  }\n\n  remove(t: T): void {\n    const i = this.indexOf(t);\n    if (i > -1) {\n      this.list.splice(i, 1);\n    }\n  }\n\n  get(i: number): T {\n    return this.list[i];\n  }\n\n  get length(): number {\n    return this.list.length;\n  }\n}\n"
  },
  {
    "path": "libs/osu/core/src/utils/index.spec.ts",
    "content": "import { determineDefaultPlaybackSpeed } from \"./index\";\n\ndescribe(\"determineDefaultPlaybackSpeed\", function () {\n  it(\"DT/NC -> 1.5\", function () {\n    expect(determineDefaultPlaybackSpeed([\"NIGHT_CORE\"])).toEqual(1.5);\n    expect(determineDefaultPlaybackSpeed([\"DOUBLE_TIME\"])).toEqual(1.5);\n  });\n  it(\"HT -> 0.75\", function () {\n    expect(determineDefaultPlaybackSpeed([\"HALF_TIME\"])).toEqual(0.75);\n  });\n  it(\"Default -> 1.0\", function () {\n    expect(determineDefaultPlaybackSpeed([\"HIDDEN\"])).toEqual(1.0);\n    expect(determineDefaultPlaybackSpeed([])).toEqual(1.0);\n  });\n});\n"
  },
  {
    "path": "libs/osu/core/src/utils/index.ts",
    "content": "import { Slider } from \"../hitobjects/Slider\";\nimport { AllHitObjects, OsuHitObject } from \"../hitobjects/Types\";\nimport { OsuClassicMod } from \"../mods/Mods\";\n\nexport function normalizeHitObjects(hitObjects: OsuHitObject[]): Record<string, AllHitObjects> {\n  const hitObjectById: Record<string, AllHitObjects> = {};\n  hitObjects.forEach((h) => {\n    hitObjectById[h.id] = h;\n    if (h instanceof Slider) {\n      hitObjectById[h.head.id] = h.head;\n      for (const c of h.checkPoints) {\n        hitObjectById[c.id] = c;\n      }\n    }\n  });\n  return hitObjectById;\n}\n\nexport function determineDefaultPlaybackSpeed(mods: OsuClassicMod[]) {\n  for (let i = 0; i < mods.length; i++) {\n    if (mods[i] === \"DOUBLE_TIME\" || mods[i] === \"NIGHT_CORE\") return 1.5;\n    if (mods[i] === \"HALF_TIME\") return 0.75;\n  }\n  return 1.0;\n}\n"
  },
  {
    "path": "libs/osu/core/test/PathApproximator.spec.ts",
    "content": "import { Vec2 } from \"@osujs/math\";\nimport { PathApproximator } from \"../src/hitobjects/slider/PathApproximator\";\nimport { buildBeatmap, parseBlueprint } from \"../src\";\nimport { assertPositionEqual } from \"./utils/asserts\";\n\n/**\n * 0 = Vector2 {x: -109,\ny: -7}\n 1 = Vector2 {x: -78,\ny: 12}\n *\n * */\n\ndescribe(\"PathApproximator\", function () {\n  describe(\"bezier\", function () {\n    it(\"[ (-109,-7), [-78, 12] ] should return good\", function () {\n      const p = [new Vec2(-109, -7), new Vec2(-78, 12)];\n      const q = PathApproximator.approximateBezier(p);\n\n      // p shoudl be equal to q (tested against osu-framework)\n    });\n  });\n});\n\ntest(\"Aether Realm One Perfect Curve Slider\", () => {\n  // Aether Realm The Sun The Moon The Star by ItsWinter\n  // At position 14:36.377 there is a very tricky slider\n  const data = `\nosu file format v14\n\n[General]\nStackLeniency: 0.6\n[Difficulty]\nHPDrainRate:5\nCircleSize:4.2\nOverallDifficulty:9.5\nApproachRate:9.8\nSliderMultiplier:1.6\nSliderTickRate:1\n[TimingPoints]\n826485,285.714285714286,3,3,1,60,1,0\n876199,-100,3,3,1,65,0,1\n[HitObjects]\n196,210,876199,2,0,P|256:227|342:197,1,120`;\n  const blueprint = parseBlueprint(data);\n  const beatmap = buildBeatmap(blueprint, { addStacking: true, mods: [] });\n  const slider = beatmap.getSlider(\"0\");\n\n  expect(slider.checkPoints.length).toEqual(1);\n\n  const tick = slider.checkPoints[0];\n\n  const progress = (tick.hitTime - slider.startTime) / slider.duration;\n  // console.log(\"tick.hitTime\", tick.hitTime);\n  // console.log(\"progress: \", progress);\n  const tickPosition = slider.ballPositionAt(progress);\n\n  // console.log(tickPosition);\n  // TODO: When using floats in Vec2 this will match up to 5 digits which we shoudl also use here.\n  assertPositionEqual(tickPosition, { x: 292.47076, y: 222.67848 }, 4);\n  // Expected (292.47076, 222.67848)\n});\n"
  },
  {
    "path": "libs/osu/core/test/utils/asserts.ts",
    "content": "import { Position } from \"@osujs/math\";\n\n// TODO: Move this to a common lib/package\nexport function assertPositionEqual(actual: Position, expected: Position, numDigits?: number) {\n  expect(actual.x).toBeCloseTo(expected.x, numDigits);\n  expect(actual.y).toBeCloseTo(expected.y, numDigits);\n}\n\nexport function arrayEqual<T>(a: T[], b: T[]) {\n  if (a.length !== b.length) return false;\n  for (let i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) {\n      return false;\n    }\n  }\n  return true;\n}\n"
  },
  {
    "path": "libs/osu/core/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/core/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"strict\": true,\n    \"declaration\": true,\n    \"types\": []\n  },\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu/core/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"test/utils/*.ts\",\n    \"**/*.spec.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu/math/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu/math/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/math/README.md",
    "content": "# \n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test osu-math` to execute the unit tests via [Jest](https://jestjs.io).\n"
  },
  {
    "path": "libs/osu/math/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-math\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu/math\",\n};\n"
  },
  {
    "path": "libs/osu/math/package.json",
    "content": "{\n  \"name\": \"@osujs/math\",\n  \"version\": \"0.0.2\"\n}\n"
  },
  {
    "path": "libs/osu/math/src/Vec2.ts",
    "content": "import { float32, float32_div, float32_mul } from \"./float32\";\n\n\n// The general type for {x, y} coordinates.\nexport type Position = { x: number; y: number };\n\n// TODO: Using 32-bit float as return result everywhere?\n// For example Vector2.Length is returned as float\nexport class Vec2 {\n  x: number;\n  y: number;\n\n  static Zero = new Vec2(0, 0);\n\n  constructor(x: number, y: number) {\n    this.x = float32(x);\n    this.y = float32(y);\n  }\n\n  // This should be preferred since it avoids using sqrt\n  // TODO: however this might be TOO precise that we will have matching issue with osu!lazer\n  static withinDistance(a: Position, b: Position, d: number): boolean {\n    return (a.x - b.x) ** 2 + (a.y - b.y) ** 2 <= d ** 2;\n  }\n\n  // returns float\n  static distance(a: Position, b: Position): number {\n    return Math.fround(Math.sqrt(Vec2.distanceSquared(a, b)));\n  }\n\n  static distanceSquared(a: Position, b: Position): number {\n    const dx = a.x - b.x,\n      dy = a.y - b.y;\n    return dx ** 2 + dy ** 2;\n  }\n\n  static equal(a: Position, b: Position): boolean {\n    // I commented out my original solution and replaced it with osu!framework variant (which is very strict)\n    // return floatEqual(a.x, b.x) && floatEqual(a.y, b.y);\n    return a.x === b.x && a.y === b.y;\n  }\n\n  static add(a: Position, b: Position): Vec2 {\n    return new Vec2(float32(a.x) + float32(b.x), float32(a.y) + float32(b.y));\n  }\n\n  static dot(a: Position, b: Position): number {\n    return Math.fround(a.x * b.x + a.y * b.y);\n  }\n\n  static sub(a: Position, b: Position): Vec2 {\n    return new Vec2(float32(a.x) - float32(b.x), float32(a.y) - float32(b.y));\n  }\n\n  // c: float\n  static scale(a: Position, c: number): Vec2 {\n    return new Vec2(float32_mul(a.x, c), float32_mul(a.y, c));\n  }\n\n  // c: float\n  static divide(a: Position, c: number): Vec2 {\n    return new Vec2(float32_div(a.x, c), float32_div(a.y, c));\n  }\n\n  // Order is important\n  static interpolate(a: Position, b: Position, p: number): Vec2 {\n    return Vec2.add(a, Vec2.sub(b, a).scale(p));\n  }\n\n  add(b: Position): Vec2 {\n    return Vec2.add(this, b);\n  }\n\n  sub(b: Position): Vec2 {\n    return Vec2.sub(this, b);\n  }\n\n  divide(c: number): Vec2 {\n    return Vec2.divide(this, c);\n  }\n\n  scale(c: number): Vec2 {\n    return Vec2.scale(this, c);\n  }\n\n  lengthSquared(): number {\n    return this.x * this.x + this.y * this.y;\n  }\n\n  length(): number {\n    return float32(Math.sqrt(this.x ** 2 + this.y ** 2));\n  }\n\n  equals(b: Position): boolean {\n    return Vec2.equal(this, b);\n  }\n\n  normalized(): Vec2 {\n    const num = this.length();\n    this.x = float32_div(this.x, num);\n    this.y = float32_div(this.y, num);\n    return this;\n  }\n}\n"
  },
  {
    "path": "libs/osu/math/src/colors.ts",
    "content": "export type RGB = [number, number, number];\n\nexport function rgbToInt(rgb: number[]): number {\n  if (rgb.length < 3) {\n    throw Error(\"Not at least three values provided\");\n  }\n  let val = 0;\n  for (let i = 0; i < 3; i++) {\n    val = val * 256 + rgb[i];\n  }\n  return val;\n}\n"
  },
  {
    "path": "libs/osu/math/src/difficulty.spec.ts",
    "content": "import { approachRateToApproachDuration, circleSizeToScale } from \"./difficulty\";\n\ntest(\"approachRateToApproachDuration\", function () {\n  expect(approachRateToApproachDuration(10)).toEqual(450);\n  expect(approachRateToApproachDuration(5)).toEqual(1200);\n  expect(approachRateToApproachDuration(0)).toEqual(1800);\n});\n\ntest(\"circleSizeToScale\", function () {\n  expect(circleSizeToScale(4)).toBeCloseTo(0.57, 5);\n});\n"
  },
  {
    "path": "libs/osu/math/src/difficulty.ts",
    "content": "/**\n * Converts the circle size to a normalized scaling value.\n * @param CS the circle size value\n */\nimport { float32 } from \"./float32\";\n\nexport function circleSizeToScale(CS: number) {\n  return float32((1.0 - (0.7 * (CS - 5)) / 5) / 2);\n}\n\n// Just a helper function that is commonly used for OD, AR calculation\nexport function difficultyRange(difficulty: number, min: number, mid: number, max: number): number {\n  if (difficulty > 5.0) return mid + ((max - mid) * (difficulty - 5.0)) / 5.0;\n  return difficulty < 5.0 ? mid - ((mid - min) * (5.0 - difficulty)) / 5.0 : mid;\n}\n\n// Minimum preempt time at AR=10\nexport const PREEMPT_MIN = 450;\n\n/**\n * Returns the approach duration depending on the abstract AR value.\n * @param AR the approach rate value\n */\nexport function approachRateToApproachDuration(AR: number) {\n  return difficultyRange(AR, 1800, 1200, PREEMPT_MIN);\n}\n\nexport function approachDurationToApproachRate(approachDurationInMs: number) {\n  return approachDurationInMs > 1200 ? (1800 - approachDurationInMs) / 120 : (1200 - approachDurationInMs) / 150 + 5;\n}\n\nexport function difficultyRangeForOd(difficulty: number, range: { od0: number; od5: number; od10: number }): number {\n  return difficultyRange(difficulty, range.od0, range.od5, range.od10);\n}\n\nexport function hitWindowGreatToOD(hitWindowGreat: number) {\n  return (80 - hitWindowGreat) / 6;\n}\n\n// HitWindows is just an array of four numbers, for example [5, 10, 15, 100] means:\n// * [-5, 5] is HitWindow of 300\n// * [-10, -5) and (+5, 10] is HitWindow of 100 and so on....\ntype HitWindows = number[];\n\nconst OSU_STD_HIT_WINDOW_RANGES: [number, number, number][] = [\n  [80, 50, 20], // Great = 300\n  [140, 100, 60], // Ok = 100\n  [200, 150, 100], // Meh = 50\n  [400, 400, 400], // Miss\n];\n\n/**\n * Returns the hit windows in the following order:\n * [Hit300, Hit100, Hit50, HitMiss]\n * @param overallDifficulty\n * @param lazerStyle\n */\nexport function hitWindowsForOD(overallDifficulty: number, lazerStyle?: boolean): HitWindows {\n  function lazerHitWindowsForOD(od: number): HitWindows {\n    return OSU_STD_HIT_WINDOW_RANGES.map(([od0, od5, od10]) => difficultyRange(od, od0, od5, od10));\n  }\n\n  // Short explanation: currently in lazer the hit windows are actually +1ms bigger due to them using the LTE <=\n  // operator instead of LT <  <= instead of < check.\n  if (lazerStyle) {\n    return lazerHitWindowsForOD(overallDifficulty);\n  }\n  // https://github.com/ppy/osu/issues/11311\n  return lazerHitWindowsForOD(overallDifficulty).map((w) => w - 1);\n}\n\n// Lazer style\nexport function overallDifficultyToHitWindowGreat(od: number) {\n  const [od0, od5, od10] = OSU_STD_HIT_WINDOW_RANGES[0];\n  return difficultyRange(od, od0, od5, od10);\n}\n"
  },
  {
    "path": "libs/osu/math/src/easing.ts",
    "content": "// https://github.com/ppy/osu-framework/blob/master/osu.Framework/Graphics/Transforms/DefaultEasingFunction.cs\nexport enum Easing {\n  LINEAR,\n  OUT,\n  OUT_QUINT,\n  OUT_ELASTIC,\n  IN_CUBIC,\n}\n\nconst elastic_const = (2 * Math.PI) / 0.3;\nconst elastic_const2 = 0.3 / 4;\nconst back_const = 1.70158;\nconst back_const2 = back_const * 1.525;\nconst bounce_const = 1 / 2.75;\n// constants used to fix expo and elastic curves to start/end at 0/1\nconst expo_offset = Math.pow(2, -10);\nconst elastic_offset_full = Math.pow(2, -11);\nconst elastic_offset_half = Math.pow(2, -10) * Math.sin((0.5 - elastic_const2) * elastic_const);\nconst elastic_offset_quarter = Math.pow(2, -10) * Math.sin((0.25 - elastic_const2) * elastic_const);\nconst in_out_elastic_offset = Math.pow(2, -10) * Math.sin(((1 - elastic_const2 * 1.5) * elastic_const) / 1.5);\n\nexport function applyEasing(t: number, easing: Easing): number {\n  switch (easing) {\n    case Easing.LINEAR:\n      return t;\n    case Easing.OUT:\n      return t * (2 - t);\n    case Easing.OUT_QUINT:\n      return --t * t * t * t * t + 1;\n    case Easing.OUT_ELASTIC:\n      return Math.pow(2, -10 * t) * Math.sin((t - elastic_const2) * elastic_const) + 1 - elastic_offset_full * t;\n    case Easing.IN_CUBIC:\n      return t * t * t;\n  }\n  return t;\n}\n\nexport function applyInterpolation(\n  time: number,\n  startTime: number,\n  endTime: number,\n  valA: number,\n  valB: number,\n  easing = Easing.LINEAR,\n): number {\n  // Or floatEqual ...\n  if (startTime >= endTime) {\n    console.error(\"startTime should be less than endTime\");\n    return valA; // or throw Error?\n  }\n  const p = applyEasing((time - startTime) / (endTime - startTime), easing);\n  return (valB - valA) * p + valA;\n}\n\n/**\n * Linear interpolation\n * @param start start value\n * @param final final value\n * @param amount number between 0 and 1\n */\nexport function lerp(start: number, final: number, amount: number) {\n  return start + (final - start) * amount;\n}\n"
  },
  {
    "path": "libs/osu/math/src/float32.ts",
    "content": "export function float32(a: number) {\n  return Math.fround(a);\n}\n\nexport function float32_add(a: number, b: number) {\n  return float32(float32(a) + float32(b));\n}\n\nexport function float32_mul(a: number, b: number) {\n  return float32(float32(a) * float32(b));\n}\n\nexport function float32_div(a: number, b: number) {\n  return float32(float32(a) / float32(b));\n}\n\nexport function float32_sqrt(a: number) {\n  return float32(Math.sqrt(float32(a)));\n}\n"
  },
  {
    "path": "libs/osu/math/src/index.ts",
    "content": "export * from \"./colors\";\nexport * from \"./difficulty\";\nexport * from \"./easing\";\nexport * from \"./time\";\nexport * from \"./utils\";\nexport * from \"./sliders\";\nexport * from \"./Vec2\";\nexport * from \"./float32\";\n"
  },
  {
    "path": "libs/osu/math/src/sliders.ts",
    "content": "import { Position, Vec2 } from \"./Vec2\";\n\n// TODO: Maybe move this to osu/Math\n\n// Maybe this is slow because of atan2() calculation\nexport function sliderRepeatAngle(curve: Position[], isRepeatAtEnd: boolean): number {\n  if (curve.length < 2) {\n    return 0.0;\n  }\n\n  const searchStart = isRepeatAtEnd ? curve.length - 1 : 0;\n  const searchDir = isRepeatAtEnd ? -1 : +1;\n  // I think the special case happening in DrawableRepeatSlider only occurs at snaking (which we don't have right\n  // now).\n  // So TODO: implement searching for two unique points when we do snaking\n  const p1 = curve[searchStart];\n  const p2 = curve[searchStart + searchDir];\n  const direction = Vec2.sub(p2, p1);\n  return Math.atan2(direction.y, direction.x);\n}\n"
  },
  {
    "path": "libs/osu/math/src/time.spec.ts",
    "content": "import { formatGameTime } from \"./time\";\n\nconst dateHMS = (h: number, m: number, s: number, ms: number) => h * 1000 * 60 * 60 + m * 60 * 1000 + s * 1000 + ms;\n\ntest(\"formatGameTime\", () => {\n  expect(formatGameTime(dateHMS(2, 4, 5, 0))).toBe(\"2:04:05\");\n  expect(formatGameTime(dateHMS(0, 4, 5, 0))).toBe(\"4:05\");\n  expect(formatGameTime(dateHMS(0, 4, 0, 0))).toBe(\"4:00\");\n  expect(formatGameTime(dateHMS(0, 4, 0, 20), true)).toBe(\"4:00.020\");\n  expect(formatGameTime(dateHMS(0, 0, 0, 0), true)).toBe(\"0:00.000\");\n});\n"
  },
  {
    "path": "libs/osu/math/src/time.ts",
    "content": "export function addZero(value: number, digits = 2) {\n  const isNegative = Number(value) < 0;\n  let buffer = value.toString();\n  let size = 0;\n\n  // Strip minus sign if number is negative\n  if (isNegative) {\n    buffer = buffer.slice(1);\n  }\n\n  size = digits - buffer.length + 1;\n  buffer = new Array(size).join(\"0\").concat(buffer);\n\n  // Adds back minus sign if needed\n  return (isNegative ? \"-\" : \"\") + buffer;\n}\n\n// courtesy to parse-ms\nexport function parseMs(milliseconds: number) {\n  const roundTowardsZero = milliseconds > 0 ? Math.floor : Math.ceil;\n\n  return {\n    days: roundTowardsZero(milliseconds / 86400000),\n    hours: roundTowardsZero(milliseconds / 3600000) % 24,\n    minutes: roundTowardsZero(milliseconds / 60000) % 60,\n    seconds: roundTowardsZero(milliseconds / 1000) % 60,\n    milliseconds: roundTowardsZero(milliseconds) % 1000,\n    microseconds: roundTowardsZero(milliseconds * 1000) % 1000,\n    nanoseconds: roundTowardsZero(milliseconds * 1e6) % 1000,\n  };\n}\n\nexport function formatGameTime(timeInMs: number, withMs?: boolean) {\n  // new Date(timeInMs) actually considers timezone\n  const { hours, seconds, minutes, milliseconds } = parseMs(timeInMs);\n  let s = hours > 0 ? `${hours}:` : \"\";\n  s = s + (hours > 0 ? addZero(minutes) : minutes) + \":\";\n  s = s + addZero(seconds);\n  return withMs ? s + \".\" + addZero(milliseconds, 3) : s;\n}\n\nexport function beatLengthToBPM(beatLength: number) {\n  return 60 * 1000 / beatLength;\n}\n"
  },
  {
    "path": "libs/osu/math/src/utils.ts",
    "content": "export function approximatelyEqual(x: number, y: number, delta: number): boolean {\n  return Math.abs(x - y) < delta;\n}\n\n// https://github.com/ppy/osu-framework/blob/105a17bc99cad251fa730b54c615d2b0d9a409d3/osu.Framework/Utils/Precision.cs\nconst FLOAT_EPS = 1e-3;\n\nexport function floatEqual(value1: number, value2: number): boolean {\n  return approximatelyEqual(value1, value2, FLOAT_EPS);\n}\n\nconst DOUBLE_EPS = 1e-7;\n\n// Used in certain cases when x and y are `double`s\nexport function doubleEqual(x: number, y: number): boolean {\n  return approximatelyEqual(x, y, DOUBLE_EPS);\n}\n\nexport function clamp(value: number, min: number, max: number): number {\n  return Math.max(min, Math.min(value, max));\n}\n"
  },
  {
    "path": "libs/osu/math/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/math/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": []\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu/math/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu/pp/.babelrc",
    "content": "{\n  \"presets\": [\n    [\n      \"@nrwl/web/babel\",\n      {\n        \"useBuiltIns\": \"usage\"\n      }\n    ]\n  ]\n}\n"
  },
  {
    "path": "libs/osu/pp/.eslintrc.json",
    "content": "{\n  \"extends\": [\n    \"../../../.eslintrc.json\"\n  ],\n  \"ignorePatterns\": [\n    \"!**/*\"\n  ],\n  \"overrides\": [\n    {\n      \"files\": [\n        \"*.ts\",\n        \"*.tsx\",\n        \"*.js\",\n        \"*.jsx\"\n      ],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\n        \"*.ts\",\n        \"*.tsx\"\n      ],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\n        \"*.js\",\n        \"*.jsx\"\n      ],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/pp/README.md",
    "content": "# osu-pp\n\nCalculates the difficulty values / pp for the osu! std game mode.\n\nVersioning\n\n| Tag           | Commit                                                                               |\n|---------------|--------------------------------------------------------------------------------------|\n| [2021.1114]() | [9fb240](https://github.com/ppy/osu/commit/9fb2402781ad91c197d51aeec716b0000f52c4d1) |\n\n"
  },
  {
    "path": "libs/osu/pp/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-pp\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu/pp\",\n};\n"
  },
  {
    "path": "libs/osu/pp/package.json",
    "content": "{\n  \"name\": \"@osujs/pp\",\n  \"version\": \"0.0.3\"\n}\n"
  },
  {
    "path": "libs/osu/pp/src/index.ts",
    "content": "export { parseBlueprint, buildBeatmap } from \"@osujs/core\";\nexport * from \"./lib/pp\";\nexport * from \"./lib/mods\";\nexport * from \"./lib/diff\";\n"
  },
  {
    "path": "libs/osu/pp/src/lib/diff.ts",
    "content": "// Strain\nimport {\n  Beatmap,\n  determineDefaultPlaybackSpeed,\n  HitCircle,\n  isHitCircle,\n  isSlider,\n  isSpinner,\n  mostCommonBeatLength,\n  OsuClassicMod,\n  OsuHitObject,\n  Slider,\n} from \"@osujs/core\";\nimport {\n  approachDurationToApproachRate,\n  approachRateToApproachDuration,\n  beatLengthToBPM,\n  float32,\n  float32_add,\n  float32_div,\n  float32_mul,\n  hitWindowGreatToOD,\n  overallDifficultyToHitWindowGreat,\n  Position,\n  Vec2,\n} from \"@osujs/math\";\nimport { calculateAim } from \"./skills/aim\";\nimport { calculateSpeed } from \"./skills/speed\";\nimport { calculateFlashlight } from \"./skills/flashlight\";\n\nexport interface OsuDifficultyHitObject {\n  // Game clock adjusted\n  deltaTime: number;\n  startTime: number;\n  endTime: number;\n\n  strainTime: number;\n  lazyJumpDistance: number;\n  minimumJumpTime: number;\n  minimumJumpDistance: number;\n  travelDistance: number;\n  travelTime: number;\n  angle: number | null;\n  hitWindowGreat: number; // Totally unnecessary to be inside here\n\n  // Slider specific diff attributes\n  lazyTravelTime: number;\n  lazyTravelDistance: number;\n  lazyEndPosition: Position;\n}\n\n// TODO: Utility functions?\nconst startTime = (o: OsuHitObject) => (isHitCircle(o) ? o.hitTime : o.startTime);\nconst endTime = (o: OsuHitObject) => (isHitCircle(o) ? o.hitTime : o.endTime);\nconst position = (o: HitCircle | Slider) => (isHitCircle(o) ? o.position : o.head.position);\n\nconst NORMALISED_RADIUS = 50.0;\nconst maximum_slider_radius = NORMALISED_RADIUS * 2.4;\nconst assumed_slider_radius = NORMALISED_RADIUS * 1.8;\n\nfunction computeSliderCursorPosition(slider: Slider) {\n  const lazyTravelTime = slider.checkPoints[slider.checkPoints.length - 1].hitTime - slider.startTime;\n  // float\n  let lazyTravelDistance = 0;\n\n  let endTimeMin = lazyTravelTime / slider.spanDuration;\n  if (endTimeMin % 2 >= 1) endTimeMin = 1 - (endTimeMin % 1);\n  else endTimeMin %= 1;\n  // temporary lazy end position until a real result can be derived.\n  let lazyEndPosition = Vec2.add(slider.startPosition, slider.path.positionAt(endTimeMin)) as Position;\n  let currCursorPosition = slider.startPosition;\n  const scalingFactor = NORMALISED_RADIUS / slider.radius; // lazySliderDistance is coded to be sensitive to scaling,\n  // this makes the maths easier with the thresholds being\n  // used.\n\n  const numCheckPoints = slider.checkPoints.length;\n  // We start from 0 because the head is NOT a slider checkpoint here\n  for (let i = 0; i < numCheckPoints; i++) {\n    const currMovementObj = slider.checkPoints[i];\n\n    // This is where we have to be very careful due to osu!lazer using their VISUAL RENDERING POSITION instead of\n    // JUDGEMENT POSITION for their last tick. bruh\n    const currMovementObjPosition =\n      currMovementObj.type === \"LAST_LEGACY_TICK\" ? slider.endPosition : currMovementObj.position;\n\n    let currMovement = Vec2.sub(currMovementObjPosition, currCursorPosition);\n    let currMovementLength = scalingFactor * currMovement.length();\n\n    // Amount of movement required so that the cursor position needs to be updated.\n    let requiredMovement = assumed_slider_radius;\n\n    if (i === numCheckPoints - 1) {\n      // The end of a slider has special aim rules due to the relaxed time constraint on position.\n      // There is both a lazy end position and the actual end slider position. We assume the player takes the\n      // simpler movement. For sliders that are circular, the lazy end position may actually be farther away than the\n      // sliders true end. This code is designed to prevent buffing situations where lazy end is actually a less\n      // efficient movement.\n      const lazyMovement = Vec2.sub(lazyEndPosition, currCursorPosition);\n\n      if (lazyMovement.length() < currMovement.length()) currMovement = lazyMovement;\n\n      currMovementLength = scalingFactor * currMovement.length();\n    } else if (currMovementObj.type === \"REPEAT\") {\n      // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.\n      requiredMovement = NORMALISED_RADIUS;\n    }\n\n    if (currMovementLength > requiredMovement) {\n      // this finds the positional delta from the required radius and the current position, and updates the\n      // currCursorPosition accordingly, as well as rewarding distance.\n      currCursorPosition = Vec2.add(\n        currCursorPosition,\n        Vec2.scale(currMovement, float32_div(currMovementLength - requiredMovement, currMovementLength)),\n      );\n      currMovementLength *= (currMovementLength - requiredMovement) / currMovementLength;\n      lazyTravelDistance = float32_add(lazyTravelDistance, currMovementLength);\n    }\n\n    if (i === numCheckPoints - 1) lazyEndPosition = currCursorPosition;\n  }\n\n  return { lazyTravelTime, lazyTravelDistance, lazyEndPosition };\n}\n\nconst defaultOsuDifficultyHitObject = (): OsuDifficultyHitObject => ({\n  hitWindowGreat: 0,\n  deltaTime: 0,\n  travelTime: 0,\n  travelDistance: 0,\n  minimumJumpDistance: 0,\n  minimumJumpTime: 0,\n  lazyJumpDistance: 0,\n  strainTime: 0,\n  endTime: 0,\n  startTime: 0,\n  angle: null,\n  lazyTravelDistance: 0,\n  lazyEndPosition: Vec2.Zero,\n  lazyTravelTime: 0,\n});\n\n/**\n * Returns n OsuDifficultyHitObjects where the first one is a dummy value\n */\nfunction preprocessDifficultyHitObject(\n  hitObjects: OsuHitObject[],\n  { clockRate, overallDifficulty }: { clockRate: number; overallDifficulty: number },\n): OsuDifficultyHitObject[] {\n  const difficultyHitObjects: OsuDifficultyHitObject[] = [defaultOsuDifficultyHitObject()];\n  const min_delta_time = 25;\n\n  const clockAdjusted = (x: number) => x / clockRate;\n  // The full hit window for \"Great\"\n  const hitWindowGreat = clockAdjusted(2 * overallDifficultyToHitWindowGreat(overallDifficulty));\n  // Caching for the sliders\n  const sliderCursorPosition: Record<string, ReturnType<typeof computeSliderCursorPosition>> = {};\n\n  function computeSliderCursorPositionIfNeeded(s: Slider): ReturnType<typeof computeSliderCursorPosition> {\n    if (sliderCursorPosition[s.id] === undefined) {\n      sliderCursorPosition[s.id] = computeSliderCursorPosition(s);\n    }\n    return sliderCursorPosition[s.id];\n  }\n\n  for (let i = 1; i < hitObjects.length; i++) {\n    const lastLast: OsuHitObject | undefined = hitObjects[i - 2];\n    const last = hitObjects[i - 1];\n    const current = hitObjects[i];\n\n    const difficultyHitObject = (function calculateDifficultyHitObject() {\n      // OsuDifficultyHitObject#Constructor\n      let result = defaultOsuDifficultyHitObject();\n      result.startTime = clockAdjusted(startTime(current));\n      result.endTime = clockAdjusted(endTime(current));\n      result.deltaTime = clockAdjusted(startTime(current) - startTime(last));\n\n      const strainTime = Math.max(result.deltaTime, min_delta_time);\n      result.strainTime = strainTime;\n      result.hitWindowGreat = hitWindowGreat;\n\n      // setDistances(clockRate)\n      if (isSlider(current)) {\n        result = { ...result, ...computeSliderCursorPosition(current) };\n        // Bonus for repeat sliders until a better per nested object strain system can be achieved.\n        result.travelDistance = result.lazyTravelDistance * Math.pow(1 + current.repeatCount / 2.5, 1.0 / 2.5);\n        result.travelTime = Math.max(result.lazyTravelTime / clockRate, min_delta_time);\n      }\n      if (isSpinner(current) || isSpinner(last)) return result;\n\n      // float\n      let scalingFactor = float32_div(NORMALISED_RADIUS, current.radius);\n      // Now current is either HitCircle or Slider\n\n      if (current.radius < 30) {\n        const smallCircleBonus = float32_div(Math.min(30 - current.radius, 5), 50);\n        scalingFactor *= 1 + smallCircleBonus;\n      }\n\n      function getEndCursorPosition(o: HitCircle | Slider) {\n        if (isHitCircle(o)) return o.position;\n        const { lazyEndPosition } = computeSliderCursorPositionIfNeeded(o);\n        return lazyEndPosition ?? o.startPosition; // TODO: How can it be nullable?\n      }\n\n      const lastCursorPosition = getEndCursorPosition(last);\n      // sqrt((x1*c-x2*c)^2+(y1*c-y2*c)^2) = sqrt(c^2 (x1-x2)^2 + c^2 (y1-y2)^2) = c * dist((x1,y1),(x2,y2))\n      result.lazyJumpDistance = Vec2.distance(\n        Vec2.scale(position(current), scalingFactor),\n        Vec2.scale(lastCursorPosition, scalingFactor),\n      );\n      result.minimumJumpTime = strainTime;\n      result.minimumJumpDistance = result.lazyJumpDistance;\n\n      if (isSlider(last)) {\n        const { lazyTravelTime: lastSliderLazyTravelTime } = computeSliderCursorPositionIfNeeded(last);\n        const lastTravelTime = Math.max(clockAdjusted(lastSliderLazyTravelTime), min_delta_time);\n        result.minimumJumpTime = Math.max(strainTime - lastTravelTime, min_delta_time);\n        const tailJumpDistance = float32(Vec2.distance(last.endPosition, position(current)) * scalingFactor);\n        result.minimumJumpDistance = Math.max(\n          0,\n          Math.min(\n            result.lazyJumpDistance - (maximum_slider_radius - assumed_slider_radius),\n            tailJumpDistance - maximum_slider_radius,\n          ),\n        );\n      }\n\n      if (lastLast !== undefined && !isSpinner(lastLast)) {\n        const lastLastCursorPosition = getEndCursorPosition(lastLast);\n        const v1 = Vec2.sub(lastLastCursorPosition, position(last));\n        const v2 = Vec2.sub(position(current), lastCursorPosition);\n        const dot = float32(Vec2.dot(v1, v2));\n        const det = float32_add(float32_mul(v1.x, v2.y), -float32_mul(v1.y, v2.x));\n        result.angle = Math.abs(Math.atan2(det, dot));\n      }\n\n      return result;\n    })();\n\n    difficultyHitObjects.push(difficultyHitObject);\n  }\n  return difficultyHitObjects;\n}\n\nexport interface DifficultyAttributes {\n  aimDifficulty: number;\n  speedDifficulty: number;\n  speedNoteCount: number;\n  flashlightDifficulty: number;\n  sliderFactor: number;\n\n  starRating: number;\n\n  // Can be easily calculated from beatmap\n  maxCombo: number;\n  hitCircleCount: number;\n  sliderCount: number;\n  spinnerCount: number;\n  // Just interesting\n  mostCommonBPM: number;\n\n  // Notice that these values can exceed 10 (when DT mod is used)\n  approachRate: number;\n  overallDifficulty: number;\n  // Surprisingly, HP is also used but only for the \"Blinds\" mod\n  drainRate: number;\n}\n\nfunction determineMaxCombo(hitObjects: OsuHitObject[]) {\n  let maxCombo = 0;\n  let hitCircleCount = 0,\n    sliderCount = 0,\n    spinnerCount = 0;\n\n  for (const o of hitObjects) {\n    maxCombo++;\n    if (isHitCircle(o)) hitCircleCount++;\n    if (isSpinner(o)) spinnerCount++;\n    if (isSlider(o)) {\n      sliderCount++;\n      maxCombo += o.checkPoints.length;\n    }\n  }\n  return { maxCombo, hitCircleCount, sliderCount, spinnerCount };\n}\n\n// This is being adjusted to keep the final pp value scaled around what it used to be when changing things.\nexport const PERFORMANCE_BASE_MULTIPLIER = 1.14;\nconst DIFFICULTY_MULTIPLIER = 0.0675;\n\nconst speedAdjustedAR = (AR: number, clockRate: number) =>\n  approachDurationToApproachRate(approachRateToApproachDuration(AR) / clockRate);\nconst speedAdjustedOD = (OD: number, clockRate: number) =>\n  hitWindowGreatToOD(overallDifficultyToHitWindowGreat(OD) / clockRate);\n\n// Calculates the different star ratings after every hit object i\nexport function calculateDifficultyAttributes(\n  { appliedMods: mods, difficulty, hitObjects, controlPointInfo }: Beatmap,\n  onlyFinalValue: boolean,\n) {\n  const clockRate = determineDefaultPlaybackSpeed(mods);\n  const diffs = preprocessDifficultyHitObject(hitObjects, {\n    clockRate,\n    overallDifficulty: difficulty.overallDifficulty,\n  });\n\n  const hasHiddenMod = mods.includes(\"HIDDEN\");\n\n  const aimValues = calculateAim(hitObjects, diffs, true, onlyFinalValue);\n  const aimValuesNoSliders = calculateAim(hitObjects, diffs, false, onlyFinalValue);\n  const { speedValues, relevantNoteCounts } = calculateSpeed(hitObjects, diffs, onlyFinalValue);\n  const flashlightValues = calculateFlashlight(\n    hitObjects,\n    diffs,\n    {\n      hasHiddenMod,\n      approachRate: difficulty.approachRate,\n    },\n    onlyFinalValue,\n  );\n\n  // Static values\n  const { hitCircleCount, sliderCount, spinnerCount, maxCombo } = determineMaxCombo(hitObjects);\n  const overallDifficulty = speedAdjustedOD(difficulty.overallDifficulty, clockRate);\n  const approachRate = speedAdjustedAR(difficulty.approachRate, clockRate);\n  const beatLength = mostCommonBeatLength({ hitObjects, timingPoints: controlPointInfo.timingPoints.list });\n  const mostCommonBPM = (beatLength === undefined ? 0 : beatLengthToBPM(beatLength)) * clockRate;\n\n  const attributes: DifficultyAttributes[] = [];\n  for (let i = 0; i < aimValues.length; i++) {\n    let aimRating = Math.sqrt(aimValues[i]) * DIFFICULTY_MULTIPLIER;\n    const aimRatingNoSliders = Math.sqrt(aimValuesNoSliders[i]) * DIFFICULTY_MULTIPLIER;\n    let speedRating = Math.sqrt(speedValues[i]) * DIFFICULTY_MULTIPLIER;\n    const speedNotes = relevantNoteCounts[i];\n    let flashlightRating = Math.sqrt(flashlightValues[i]) * DIFFICULTY_MULTIPLIER;\n\n    const sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;\n\n    // TODO: TouchDevice mod?\n    if (mods.includes(\"TOUCH_DEVICE\" as OsuClassicMod)) {\n      aimRating = Math.pow(aimRating, 0.8);\n      flashlightRating = Math.pow(flashlightRating, 0.8);\n    }\n\n    if (mods.includes(\"RELAX\")) {\n      aimRating *= 0.9;\n      speedRating = 0;\n      flashlightRating *= 0.7;\n    }\n    const baseAimPerformance = Math.pow(5 * Math.max(1, aimRating / 0.0675) - 4, 3) / 100000;\n    const baseSpeedPerformance = Math.pow(5 * Math.max(1, speedRating / 0.0675) - 4, 3) / 100000;\n    let baseFlashlightPerformance = 0.0;\n\n    if (mods.includes(\"FLASH_LIGHT\")) baseFlashlightPerformance = Math.pow(flashlightRating, 2.0) * 25.0;\n\n    const basePerformance = Math.pow(\n      Math.pow(baseAimPerformance, 1.1) +\n        Math.pow(baseSpeedPerformance, 1.1) +\n        Math.pow(baseFlashlightPerformance, 1.1),\n      1.0 / 1.1,\n    );\n\n    const starRating =\n      basePerformance > 0.00001\n        ? Math.cbrt(PERFORMANCE_BASE_MULTIPLIER) *\n          0.027 *\n          (Math.cbrt((100000 / Math.pow(2, 1 / 1.1)) * basePerformance) + 4)\n        : 0;\n\n    attributes.push({\n      aimDifficulty: aimRating,\n      speedDifficulty: speedRating,\n      speedNoteCount: speedNotes,\n      flashlightDifficulty: flashlightRating,\n      sliderFactor,\n      starRating,\n      // These are actually redundant but idc\n      hitCircleCount,\n      sliderCount,\n      spinnerCount,\n      maxCombo,\n      overallDifficulty,\n      approachRate,\n      mostCommonBPM,\n      drainRate: difficulty.drainRate,\n    });\n  }\n  return attributes;\n}\n"
  },
  {
    "path": "libs/osu/pp/src/lib/mods.spec.ts",
    "content": "import { normalizeModsForSR } from \"./mods\";\n\ndescribe(\"Mods reduction\", function () {\n  it(\"FL,HD -> HD,FL\", function () {\n    expect(normalizeModsForSR([\"HIDDEN\", \"FLASH_LIGHT\"])).toEqual([\"FLASH_LIGHT\", \"HIDDEN\"]);\n  });\n  // Tests sorting\n  it(\"HD,FL -> FL,HD\", function () {\n    expect(normalizeModsForSR([\"FLASH_LIGHT\", \"HIDDEN\"])).toEqual([\"FLASH_LIGHT\", \"HIDDEN\"]);\n  });\n\n  it(\"Some cases\", function () {\n    expect(normalizeModsForSR([])).toEqual([]);\n    expect(normalizeModsForSR([\"NO_FAIL\"])).toEqual([]);\n    expect(normalizeModsForSR([\"HIDDEN\", \"HARD_ROCK\"])).toEqual([\"HARD_ROCK\"]);\n    expect(normalizeModsForSR([\"DOUBLE_TIME\", \"HARD_ROCK\", \"HIDDEN\", \"FLASH_LIGHT\"])).toEqual([\n      \"DOUBLE_TIME\",\n      \"FLASH_LIGHT\",\n      \"HARD_ROCK\",\n      \"HIDDEN\",\n    ]);\n  });\n});\n"
  },
  {
    "path": "libs/osu/pp/src/lib/mods.ts",
    "content": "import { OsuClassicMod } from \"@osujs/core\";\n\nconst modsThatAffectSR = (flashlightIncluded: boolean): OsuClassicMod[] => {\n  const defaultMods: OsuClassicMod[] = [\n    // TOUCH_DEVICE\n    \"DOUBLE_TIME\", // \"NIGHT_CORE\",\n    \"HALF_TIME\",\n    \"EASY\",\n    \"HARD_ROCK\",\n    \"FLASH_LIGHT\",\n    \"RELAX\",\n  ];\n  if (flashlightIncluded) {\n    defaultMods.push(\"HIDDEN\");\n  }\n  return defaultMods;\n};\n\n/**\n * Normalizes the list of given mods to standardised ones that actually affect the star rating.\n * Also sorts them accordingly so that they can be used as a cache key.\n *\n * Intended as a performance optimization so that the user can cache the star rating for certain mod combinations.\n *\n * Since the newest SR changes, HD also affects star rating combined with FL.\n *\n * @example [\"HD\", \"HR\", \"NF\"] -> [\"HR\"]\n * @example [\"NC\"] -> [\"DT\"]\n * @example [\"HD\", \"FL\"] -> [\"HD\", \"FL\"]\n */\nexport function normalizeModsForSR(mods: OsuClassicMod[]): OsuClassicMod[] {\n  const includesFL = mods.includes(\"FLASH_LIGHT\");\n  const modsFilter = modsThatAffectSR(includesFL);\n  return mods.filter((m) => modsFilter.includes(m)).sort();\n}\n"
  },
  {
    "path": "libs/osu/pp/src/lib/pp.ts",
    "content": "import { OsuClassicMod, osuStableAccuracy } from \"@osujs/core\";\nimport { clamp } from \"@osujs/math\";\nimport { DifficultyAttributes, PERFORMANCE_BASE_MULTIPLIER } from \"./diff\";\n\ninterface OsuPerformanceAttributes {\n  // The \"PP\" value that is then displayed and used for calculations\n  total: number;\n\n  aim: number;\n  speed: number;\n  accuracy: number;\n  flashlight: number;\n  effectiveMissCount: number;\n}\n\n// parameters extracted from ScoreInfo\nexport interface ScoreParams {\n  mods: OsuClassicMod[];\n  maxCombo: number;\n  countGreat: number;\n  countOk: number;\n  countMeh: number;\n  countMiss: number;\n}\n\nexport function calculatePerformanceAttributes(\n  beatmapParams: DifficultyAttributes,\n  scoreParams: ScoreParams,\n): OsuPerformanceAttributes {\n  const {\n    hitCircleCount,\n    sliderCount,\n    spinnerCount,\n    aimDifficulty,\n    speedDifficulty,\n    flashlightDifficulty,\n    approachRate,\n    drainRate,\n    overallDifficulty,\n    sliderFactor,\n    speedNoteCount,\n    maxCombo: beatmapMaxCombo,\n  } = beatmapParams;\n  const { mods, countMeh, countGreat, countMiss, countOk, maxCombo: scoreMaxCombo } = scoreParams;\n  const accuracy = osuStableAccuracy([countGreat, countOk, countMeh, countMiss]) ?? 0;\n  const totalHits = countGreat + countOk + countMeh + countMiss;\n\n  let effectiveMissCount = (function calculateEffectiveMissCount() {\n    // Guess the number of misses + slider breaks from combo\n    let comboBasedMissCount = 0.0;\n\n    if (sliderCount > 0) {\n      const fullComboThreshold = beatmapMaxCombo - 0.1 * sliderCount;\n      if (scoreMaxCombo < fullComboThreshold) comboBasedMissCount = fullComboThreshold / Math.max(1.0, scoreMaxCombo);\n    }\n    // Clamp misscount since it's derived from combo and can be higher than total hits and that breaks some calculations\n    comboBasedMissCount = Math.min(comboBasedMissCount, countOk + countMiss + countMiss);\n    return Math.max(countMiss, comboBasedMissCount);\n  })();\n\n  let multiplier = PERFORMANCE_BASE_MULTIPLIER;\n  if (mods.includes(\"NO_FAIL\")) multiplier *= Math.max(0.9, 1 - 0.02 * effectiveMissCount);\n  if (mods.includes(\"SPUN_OUT\")) multiplier *= 1.0 - Math.pow(spinnerCount / totalHits, 0.85);\n  if (mods.includes(\"RELAX\")) {\n    const okMultiplier = Math.max(0.0, overallDifficulty > 0 ? 1 - Math.pow(overallDifficulty / 13.33, 1.8) : 1.0);\n    const mehMultiplier = Math.max(0.0, overallDifficulty > 0 ? 1 - Math.pow(overallDifficulty / 13.33, 5.0) : 1.0);\n    effectiveMissCount = Math.min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);\n  }\n\n  const comboScalingFactor =\n    beatmapMaxCombo <= 0 ? 1.0 : Math.min(Math.pow(scoreMaxCombo, 0.8) / Math.pow(beatmapMaxCombo, 0.8), 1.0);\n\n  const aimValue = (function computeAimValue() {\n    let aimValue = Math.pow(5.0 * Math.max(1.0, aimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;\n\n    const lengthBonus =\n      0.95 + 0.4 * Math.min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.log10(totalHits / 2000.0) * 0.5 : 0.0);\n    aimValue *= lengthBonus;\n    if (effectiveMissCount > 0)\n      aimValue *= 0.97 * Math.pow(1 - Math.pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount);\n    aimValue *= comboScalingFactor;\n\n    let approachRateFactor = 0.0;\n    if (approachRate > 10.33) approachRateFactor = 0.3 * (approachRate - 10.33);\n    else if (approachRate < 8) approachRateFactor = 0.05 * (8.0 - approachRate);\n\n    if (mods.includes(\"RELAX\")) approachRateFactor = 0.0;\n\n    aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for long maps with high AR\n\n    if (mods.includes(\"BLINDS\" as OsuClassicMod)) {\n      aimValue *=\n        1.3 +\n        totalHits *\n          (0.0016 / (1 + 2 * effectiveMissCount)) *\n          Math.pow(accuracy, 16) *\n          (1 - 0.003 * drainRate * drainRate);\n    } else if (mods.includes(\"HIDDEN\")) {\n      // Rewarding low AR when there is HD -> this nerfs high AR and buffs low AR\n      aimValue *= 1.0 + 0.04 * (12.0 - approachRate);\n    }\n\n    // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.\n    const estimateDifficultSliders = sliderCount * 0.15;\n    if (sliderCount > 0) {\n      const estimateSliderEndsDropped = clamp(\n        Math.min(countOk + countMeh + countMiss, beatmapMaxCombo - scoreMaxCombo),\n        0,\n        estimateDifficultSliders,\n      );\n      const sliderNerfFactor =\n        (1 - sliderFactor) * Math.pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + sliderFactor;\n      aimValue *= sliderNerfFactor;\n    }\n    aimValue *= accuracy;\n    aimValue *= 0.98 + Math.pow(overallDifficulty, 2) / 2500;\n\n    return aimValue;\n  })();\n  const speedValue = (function computeSpeedValue() {\n    if (mods.includes(\"RELAX\")) {\n      return 0.0;\n    }\n    let speedValue = Math.pow(5.0 * Math.max(1.0, speedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;\n    const lengthBonus =\n      0.95 + 0.4 * Math.min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.log10(totalHits / 2000.0) * 0.5 : 0.0);\n    speedValue *= lengthBonus;\n\n    // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of\n    // misses.\n    if (effectiveMissCount > 0)\n      speedValue *=\n        0.97 * Math.pow(1 - Math.pow(effectiveMissCount / totalHits, 0.775), Math.pow(effectiveMissCount, 0.875));\n\n    speedValue *= comboScalingFactor;\n\n    let approachRateFactor = 0.0;\n    if (approachRate > 10.33) approachRateFactor = 0.3 * (approachRate - 10.33);\n\n    speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.\n\n    if (mods.includes(\"BLINDS\" as OsuClassicMod)) {\n      // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.\n      speedValue *= 1.12;\n    } else if (mods.includes(\"HIDDEN\")) {\n      // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.\n      speedValue *= 1.0 + 0.04 * (12.0 - approachRate);\n    }\n\n    // Calculate accuracy assuming the worst case scenario\n    const relevantTotalDiff = totalHits - speedNoteCount;\n    const relevantCountGreat = Math.max(0, countGreat - relevantTotalDiff);\n    const relevantCountOk = Math.max(0, countOk - Math.max(0, relevantTotalDiff - countGreat));\n    const relevantCountMeh = Math.max(0, countMeh - Math.max(0, relevantTotalDiff - countGreat - countOk));\n    const relevantAccuracy =\n      speedNoteCount == 0\n        ? 0\n        : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (speedNoteCount * 6.0);\n\n    // Scale the speed value with accuracy and OD.\n    speedValue *=\n      (0.95 + Math.pow(overallDifficulty, 2) / 750) *\n      Math.pow((accuracy + relevantAccuracy) / 2, (14.5 - Math.max(overallDifficulty, 8)) / 2);\n\n    // Scale the speed value with # of 50s to punish double-tapping.\n    speedValue *= Math.pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);\n\n    return speedValue;\n  })();\n  const accuracyValue = (function computeAccuracyValue() {\n    if (mods.includes(\"RELAX\")) return 0.0;\n\n    // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the\n    // timing hit window.\n    let betterAccuracyPercentage;\n    const amountHitObjectsWithAccuracy = hitCircleCount;\n\n    if (amountHitObjectsWithAccuracy > 0)\n      betterAccuracyPercentage =\n        ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) /\n        (amountHitObjectsWithAccuracy * 6.0);\n    else betterAccuracyPercentage = 0;\n\n    // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points.\n    if (betterAccuracyPercentage < 0) betterAccuracyPercentage = 0;\n\n    // Lots of arbitrary values from testing.\n    // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.\n    let accuracyValue = Math.pow(1.52163, overallDifficulty) * Math.pow(betterAccuracyPercentage, 24) * 2.83;\n\n    // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.\n    accuracyValue *= Math.min(1.15, Math.pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));\n\n    // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.\n    if (mods.includes(\"BLINDS\" as OsuClassicMod)) accuracyValue *= 1.14;\n    else if (mods.includes(\"HIDDEN\")) accuracyValue *= 1.08;\n\n    if (mods.includes(\"FLASH_LIGHT\")) accuracyValue *= 1.02;\n\n    return accuracyValue;\n  })();\n\n  const flashlightValue = (function computeFlashLightValue() {\n    if (!mods.includes(\"FLASH_LIGHT\")) return 0.0;\n\n    let flashlightValue = Math.pow(flashlightDifficulty, 2.0) * 25.0;\n\n    // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of\n    // misses.\n    if (effectiveMissCount > 0)\n      flashlightValue *=\n        0.97 * Math.pow(1 - Math.pow(effectiveMissCount / totalHits, 0.775), Math.pow(effectiveMissCount, 0.875));\n\n    flashlightValue *= comboScalingFactor;\n\n    // Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.\n    flashlightValue *=\n      0.7 +\n      0.1 * Math.min(1.0, totalHits / 200.0) +\n      (totalHits > 200 ? 0.2 * Math.min(1.0, (totalHits - 200) / 200.0) : 0.0);\n\n    // Scale the flashlight value with accuracy _slightly_.\n    flashlightValue *= 0.5 + accuracy / 2.0;\n    // It is important to also consider accuracy difficulty when doing that.\n    flashlightValue *= 0.98 + Math.pow(overallDifficulty, 2) / 2500;\n\n    return flashlightValue;\n  })();\n\n  const totalValue =\n    Math.pow(\n      Math.pow(aimValue, 1.1) +\n        Math.pow(speedValue, 1.1) +\n        Math.pow(accuracyValue, 1.1) +\n        Math.pow(flashlightValue, 1.1),\n      1.0 / 1.1,\n    ) * multiplier;\n\n  return {\n    aim: aimValue,\n    speed: speedValue,\n    accuracy: accuracyValue,\n    flashlight: flashlightValue,\n    effectiveMissCount,\n    total: totalValue,\n  };\n}\n"
  },
  {
    "path": "libs/osu/pp/src/lib/skills/aim.ts",
    "content": "import { isSlider, isSpinner, OsuHitObject } from \"@osujs/core\";\nimport { clamp } from \"@osujs/math\";\nimport { OsuDifficultyHitObject } from \"../diff\";\nimport { calculateOsuStrainDifficultyValues } from \"./strain\";\n\nconst wide_angle_multiplier = 1.5;\nconst acute_angle_multiplier = 1.95;\nconst slider_multiplier = 1.35;\nconst velocity_change_multiplier = 0.75;\n\nconst skillMultiplier = 23.55;\n\nconst strainDecayBase = 0.15;\nconst strainDecay = (ms: number) => Math.pow(strainDecayBase, ms / 1000);\n\nconst calcWideAngleBonus = (angle: number) =>\n  Math.pow(Math.sin((3.0 / 4) * (Math.min((5.0 / 6) * Math.PI, Math.max(Math.PI / 6, angle)) - Math.PI / 6)), 2);\nconst calcAcuteAngleBonus = (angle: number) => 1 - calcWideAngleBonus(angle);\n\n/**\n * @returns `strains[i]` = strain value after the `i`th hitObject\n */\nfunction calculateAimStrains(\n  hitObjects: OsuHitObject[],\n  diffs: OsuDifficultyHitObject[],\n  withSliders: boolean,\n): number[] {\n  if (hitObjects.length === 0) return [];\n\n  const strains: number[] = [0];\n  let currentStrain = 0;\n\n  // Index 0 is a dummy difficultyHitObject\n  for (let i = 1; i < diffs.length; i++) {\n    const lastLast = hitObjects[i - 2];\n    const last = hitObjects[i - 1];\n    const current = hitObjects[i];\n\n    const diffLastLast = diffs[i - 2];\n    const diffLast = diffs[i - 1];\n    const diffCurrent = diffs[i];\n\n    const strainValueOf = (function () {\n      // We need at least three non-dummy elements for this calculation\n      if (i <= 2 || isSpinner(current) || isSpinner(last)) return 0;\n\n      let currVelocity = diffCurrent.lazyJumpDistance / diffCurrent.strainTime;\n\n      if (isSlider(last) && withSliders) {\n        const travelVelocity = diffLast.travelDistance / diffLast.travelTime;\n        const movementVelocity = diffCurrent.minimumJumpDistance / diffCurrent.minimumJumpTime;\n        currVelocity = Math.max(currVelocity, movementVelocity + travelVelocity);\n      }\n\n      let prevVelocity = diffLast.lazyJumpDistance / diffLast.strainTime;\n      if (isSlider(lastLast) && withSliders) {\n        const travelVelocity = diffLastLast.travelDistance / diffLastLast.travelTime;\n        const movementVelocity = diffLast.minimumJumpDistance / diffLast.minimumJumpTime;\n        prevVelocity = Math.max(prevVelocity, movementVelocity + travelVelocity);\n      }\n      let wideAngleBonus = 0;\n      let acuteAngleBonus = 0;\n      let sliderBonus = 0;\n      let velocityChangeBonus = 0;\n\n      let aimStrain = currVelocity; // Start strain with regular velocity.\n\n      if (\n        // If rhythms are the same.\n        Math.max(diffCurrent.strainTime, diffLast.strainTime) <\n        1.25 * Math.min(diffCurrent.strainTime, diffLast.strainTime)\n      ) {\n        if (diffCurrent.angle !== null && diffLast.angle !== null && diffLastLast.angle !== null) {\n          const currAngle = diffCurrent.angle;\n          const lastAngle = diffLast.angle;\n          const lastLastAngle = diffLastLast.angle;\n\n          // Rewarding angles, take the smaller velocity as base.\n          const angleBonus = Math.min(currVelocity, prevVelocity);\n\n          wideAngleBonus = calcWideAngleBonus(currAngle);\n          acuteAngleBonus = calcAcuteAngleBonus(currAngle);\n\n          if (diffCurrent.strainTime > 100)\n            // Only buff deltaTime exceeding 300 bpm 1/2.\n            acuteAngleBonus = 0;\n          else {\n            acuteAngleBonus *=\n              calcAcuteAngleBonus(lastAngle) *\n              Math.min(angleBonus, 125 / diffCurrent.strainTime) *\n              Math.pow(Math.sin((Math.PI / 2) * Math.min(1, (100 - diffCurrent.strainTime) / 25)), 2) *\n              Math.pow(Math.sin(((Math.PI / 2) * (clamp(diffCurrent.lazyJumpDistance, 50, 100) - 50)) / 50), 2);\n          }\n\n          // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.\n          wideAngleBonus *= angleBonus * (1 - Math.min(wideAngleBonus, Math.pow(calcWideAngleBonus(lastAngle), 3)));\n          // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.\n          acuteAngleBonus *=\n            0.5 + 0.5 * (1 - Math.min(acuteAngleBonus, Math.pow(calcAcuteAngleBonus(lastLastAngle), 3)));\n        }\n      }\n\n      // TODO: floatEqual?\n      if (Math.max(prevVelocity, currVelocity) !== 0) {\n        // We want to use the average velocity over the whole object when awarding differences, not the individual jump\n        // and slider path velocities.\n        prevVelocity = (diffLast.lazyJumpDistance + diffLastLast.travelDistance) / diffLast.strainTime;\n        currVelocity = (diffCurrent.lazyJumpDistance + diffLast.travelDistance) / diffCurrent.strainTime;\n\n        // Scale with ratio of difference compared to 0.5 * max dist.\n        const distRatio = Math.pow(\n          Math.sin(((Math.PI / 2) * Math.abs(prevVelocity - currVelocity)) / Math.max(prevVelocity, currVelocity)),\n          2,\n        );\n\n        // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.\n        const overlapVelocityBuff = Math.min(\n          125 / Math.min(diffCurrent.strainTime, diffLast.strainTime),\n          Math.abs(prevVelocity - currVelocity),\n        );\n\n        // Choose the largest bonus, multiplied by ratio.\n        velocityChangeBonus = overlapVelocityBuff * distRatio;\n\n        // Penalize for rhythm changes.\n        velocityChangeBonus *= Math.pow(\n          Math.min(diffCurrent.strainTime, diffLast.strainTime) / Math.max(diffCurrent.strainTime, diffLast.strainTime),\n          2,\n        );\n      }\n\n      if (isSlider(last)) {\n        // Reward sliders based on velocity.\n        sliderBonus = diffLast.travelDistance / diffLast.travelTime;\n      }\n\n      // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.\n      aimStrain += Math.max(\n        acuteAngleBonus * acute_angle_multiplier,\n        wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier,\n      );\n\n      // Add in additional slider velocity bonus.\n      if (withSliders) aimStrain += sliderBonus * slider_multiplier;\n\n      return aimStrain;\n    })();\n\n    currentStrain *= strainDecay(diffCurrent.deltaTime);\n    currentStrain += strainValueOf * skillMultiplier;\n    strains.push(currentStrain);\n  }\n  return strains;\n}\n\nexport function calculateAim(\n  hitObjects: OsuHitObject[],\n  diffs: OsuDifficultyHitObject[],\n  withSliders: boolean,\n  onlyFinalValue: boolean,\n) {\n  const strains = calculateAimStrains(hitObjects, diffs, withSliders);\n  return calculateOsuStrainDifficultyValues(\n    diffs,\n    strains,\n    {\n      decayWeight: 0.9,\n      difficultyMultiplier: 1.06,\n      sectionDuration: 400,\n      reducedSectionCount: 10,\n      strainDecay,\n    },\n    onlyFinalValue,\n  );\n}\n"
  },
  {
    "path": "libs/osu/pp/src/lib/skills/flashlight.ts",
    "content": "import { HitCircle, isHitCircle, isSlider, isSpinner, ModHiddenConstants, OsuHitObject, Slider } from \"@osujs/core\";\nimport { OsuDifficultyHitObject } from \"../diff\";\nimport { approachRateToApproachDuration, clamp, PREEMPT_MIN, Vec2 } from \"@osujs/math\";\nimport { calculateFlashlightDifficultyValues } from \"./strain\";\n\nconst skillMultiplier = 0.052;\nconst strainDecayBase = 0.15;\n\nconst max_opacity_bonus = 0.4;\nconst hidden_bonus = 0.2;\nconst min_velocity = 0.5;\nconst slider_multiplier = 1.3;\nconst min_angle_multiplier = 0.2;\n\nconst strainDecay = (ms: number) => Math.pow(strainDecayBase, ms / 1000);\nconst startTime = (o: OsuHitObject) => (isHitCircle(o) ? o.hitTime : o.startTime);\nconst position = (o: HitCircle | Slider) => (isHitCircle(o) ? o.position : o.head.position);\nconst endPosition = (o: HitCircle | Slider) => (isHitCircle(o) ? o.position : o.endPosition);\n\n// const blueprintEndPosition = (o: HitCircle | Slider) => (isHitCircle(o) ? o.position : o.endPosition);\n\nfunction calculateFlashlightStrains(\n  hitObjects: OsuHitObject[],\n  diffs: OsuDifficultyHitObject[],\n  { hasHiddenMod, approachRate }: { hasHiddenMod: boolean; approachRate: number },\n): number[] {\n  if (hitObjects.length === 0) return [];\n\n  let currentStrain = 0;\n  const strains = [currentStrain];\n\n  for (let k = 1; k < diffs.length; k++) {\n    const thisHitObject = hitObjects[k];\n    const thisDiff = diffs[k];\n\n    const evaluateDifficultyOf = (function () {\n      if (isSpinner(thisHitObject)) return 0;\n\n      const scalingFactor = 52.0 / thisHitObject.radius;\n      let smallDistNerf = 1.0;\n      let cumulativeStrainTime = 0.0;\n      let result = 0.0;\n\n      let angleRepeatCount = 0.0;\n\n      const previousCount = Math.min(k - 1, 10);\n      for (let i = 0; i < previousCount; i++) {\n        const current = hitObjects[k - i - 1];\n        const currentDiff = diffs[k - i - 1];\n        const lastDiff = diffs[k - i];\n\n        if (isSpinner(current)) continue;\n\n        const jumpDistance = Vec2.distance(position(thisHitObject), endPosition(current));\n        cumulativeStrainTime += lastDiff.strainTime;\n        if (i === 0) smallDistNerf = Math.min(1.0, jumpDistance / 75.0);\n\n        const stackNerf = Math.min(1.0, currentDiff.lazyJumpDistance / scalingFactor / 25.0);\n\n        const opacityBonus =\n          1.0 +\n          max_opacity_bonus *\n            (1.0 - opacityAt(thisHitObject, { hidden: hasHiddenMod, approachRate, time: startTime(current) }));\n\n        result += (stackNerf * opacityBonus * scalingFactor * jumpDistance) / cumulativeStrainTime;\n\n        if (currentDiff.angle !== null && thisDiff.angle !== null) {\n          if (Math.abs(currentDiff.angle - thisDiff.angle) < 0.02) angleRepeatCount += Math.max(1.0 - 0.1 * i, 0.0);\n        }\n      }\n      result = Math.pow(smallDistNerf * result, 2.0);\n      if (hasHiddenMod) result *= 1.0 + hidden_bonus;\n\n      result *= min_angle_multiplier + (1.0 - min_angle_multiplier) / (angleRepeatCount + 1.0);\n      if (isSlider(thisHitObject)) {\n        const pixelTravelDistance = thisDiff.lazyTravelDistance / scalingFactor;\n        let sliderBonus = Math.pow(Math.max(0.0, pixelTravelDistance / thisDiff.travelTime - min_velocity), 0.5);\n        sliderBonus *= pixelTravelDistance;\n        if (thisHitObject.repeatCount > 0) {\n          sliderBonus /= thisHitObject.repeatCount + 1;\n        }\n        result += sliderBonus * slider_multiplier;\n      }\n      return result;\n    })();\n\n    currentStrain *= strainDecay(thisDiff.deltaTime);\n    currentStrain += evaluateDifficultyOf * skillMultiplier;\n    strains.push(currentStrain);\n  }\n\n  return strains;\n}\n\n// TODO: Move up and use it in pixijs\n// This is also different for SliderEndCircles\nfunction calcFadeInDuration(approachDuration: number, hidden: boolean) {\n  if (hidden) {\n    return approachDuration * ModHiddenConstants.FADE_IN_DURATION_MULTIPLIER;\n  } else {\n    return 400 * Math.min(1, approachDuration / PREEMPT_MIN);\n  }\n}\n\nfunction opacityAt(\n  hitObject: OsuHitObject,\n  { time, approachRate, hidden }: { time: number; approachRate: number; hidden: boolean },\n): number {\n  if (time > startTime(hitObject)) {\n    return 0.0;\n  }\n\n  // These numbers are actually different for SliderTick and SliderEndCircle, but the main hit objects (circle, slider, spinner)\n  // always have the same timePreempt and timeFadeIn\n  const timePreempt = approachRateToApproachDuration(approachRate);\n  const timeFadeIn = calcFadeInDuration(timePreempt, hidden);\n  const fadeInStartTime = startTime(hitObject) - timePreempt;\n  const fadeInDuration = timeFadeIn;\n\n  if (hidden) {\n    const fadeOutStartTime = startTime(hitObject) - timePreempt + timeFadeIn;\n    const fadeOutDuration = timePreempt * ModHiddenConstants.FADE_OUT_DURATION_MULTIPLIER;\n    return Math.min(\n      clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0),\n      1.0 - clamp((time - fadeOutStartTime) / fadeOutDuration, 0.0, 1.0),\n    );\n  }\n\n  return clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);\n}\n\nexport function calculateFlashlight(\n  hitObjects: OsuHitObject[],\n  diffs: OsuDifficultyHitObject[],\n  options: { hasHiddenMod: boolean; approachRate: number },\n  onlyFinalValue: boolean,\n) {\n  const strains = calculateFlashlightStrains(hitObjects, diffs, options);\n  return calculateFlashlightDifficultyValues(diffs, strains, { strainDecay, sectionDuration: 400 }, onlyFinalValue);\n}\n"
  },
  {
    "path": "libs/osu/pp/src/lib/skills/speed.ts",
    "content": "import { isSlider, isSpinner, OsuHitObject } from \"@osujs/core\";\nimport { OsuDifficultyHitObject } from \"../diff\";\nimport { clamp } from \"@osujs/math\";\nimport { calculateOsuStrainDifficultyValues } from \"./strain\";\n\nconst single_spacing_threshold = 125;\nconst rhythm_multiplier = 0.75;\nconst history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.\nconst min_speed_bonus = 75; // ~200BPM\nconst speed_balancing_factor = 40;\n\nconst skillMultiplier = 1375;\nconst strainDecayBase = 0.3;\n\nconst strainDecay = (ms: number) => Math.pow(strainDecayBase, ms / 1000);\n\ninterface SpeedInfo {\n  strains: number[];\n  relevantNoteCounts: number[];\n}\n\n/**\n * @param hitObjects\n * @param diffs\n * @param onlyFinalRelevantCountValue\n * @returns `strains[i]` = speed strain value after the `i`th hitObject\n */\nfunction calculateSpeedInfo(\n  hitObjects: OsuHitObject[],\n  diffs: OsuDifficultyHitObject[],\n  onlyFinalRelevantCountValue: boolean,\n): SpeedInfo {\n  let currentStrain = 0;\n  const strains = [0];\n\n  const relevantNoteCounts: number[] = [];\n  if (!onlyFinalRelevantCountValue) relevantNoteCounts.push(0);\n  let maxStrain = 0;\n\n  for (let i = 1; i < hitObjects.length; i++) {\n    const current = hitObjects[i];\n\n    const diffCurrent = diffs[i];\n    const diffPrev = diffs[i - 1];\n    const diffNext: OsuDifficultyHitObject | undefined = diffs[i + 1];\n\n    // Helper function so that we don't have to use the ReversedQueue `Previous`\n    const previous = (j: number) => diffs[i - 1 - j];\n    const previousHitObject = (j: number) => hitObjects[i - 1 - j];\n\n    const speedEvaluateDifficultyOf = (function () {\n      if (isSpinner(current)) return 0;\n\n      // derive strainTime for calculation\n      let strainTime = diffCurrent.strainTime;\n      let doubletapness = 1;\n\n      // Nerf doubletappable doubles\n      if (diffNext !== undefined) {\n        const currDeltaTime = Math.max(1, diffCurrent.deltaTime);\n        const nextDeltaTime = Math.max(1, diffNext.deltaTime);\n        const deltaDifference = Math.abs(nextDeltaTime - currDeltaTime);\n        const speedRatio = currDeltaTime / Math.max(currDeltaTime, deltaDifference);\n        const windowRatio = Math.pow(Math.min(1, currDeltaTime / diffCurrent.hitWindowGreat), 2);\n        doubletapness = Math.pow(speedRatio, 1 - windowRatio);\n      }\n\n      // Cap deltatime to the OD 300 hitwindow.\n      // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of\n      // the cap.\n      strainTime /= clamp(strainTime / diffCurrent.hitWindowGreat / 0.93, 0.92, 1);\n\n      // derive speedBonus for calculation\n      let speedBonus = 1.0;\n\n      if (strainTime < min_speed_bonus)\n        speedBonus = 1 + 0.75 * Math.pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);\n\n      const travelDistance = diffPrev.travelDistance;\n      const distance = Math.min(single_spacing_threshold, travelDistance + diffCurrent.minimumJumpDistance);\n\n      return (\n        ((speedBonus + speedBonus * Math.pow(distance / single_spacing_threshold, 3.5)) * doubletapness) / strainTime\n      );\n    })();\n\n    const currentRhythm = (function () {\n      if (isSpinner(current)) return 0;\n\n      let previousIslandSize = 0;\n\n      let rhythmComplexitySum = 0;\n      let islandSize = 1;\n      let startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms\n\n      let firstDeltaSwitch = false;\n\n      // TODO: i - 1 or i\n      const historicalNoteCount = Math.min(i - 1, 32);\n      let rhythmStart = 0;\n\n      // Optimization from a \"future\" commit\n      // https://github.com/ppy/osu/commit/c87ff82c1cde3af45c173fcb264de999340b743c#diff-4ed7064eeb60b6f0a19dc16729cd6fc3c3ba9794962a7bcfc830bddbea781000\n      while (\n        rhythmStart < historicalNoteCount - 2 &&\n        diffCurrent.startTime - previous(rhythmStart).startTime < history_time_max\n      )\n        rhythmStart++;\n\n      for (let j = rhythmStart; j > 0; j--) {\n        const currObj = previous(j - 1);\n        const prevObj = previous(j);\n        const lastObj = previous(j + 1);\n\n        let currHistoricalDecay = (history_time_max - (diffCurrent.startTime - currObj.startTime)) / history_time_max;\n        currHistoricalDecay = Math.min((historicalNoteCount - j) / historicalNoteCount, currHistoricalDecay);\n\n        const currDelta = currObj.strainTime;\n        const prevDelta = prevObj.strainTime;\n        const lastDelta = lastObj.strainTime;\n        const currRatio =\n          1.0 +\n          6.0 *\n            Math.min(\n              0.5,\n              Math.pow(Math.sin(Math.PI / (Math.min(prevDelta, currDelta) / Math.max(prevDelta, currDelta))), 2),\n            ); // fancy function to calculate rhythmbonuses.\n\n        let windowPenalty = Math.min(\n          1,\n          Math.max(0, Math.abs(prevDelta - currDelta) - diffCurrent.hitWindowGreat * 0.3) /\n            (diffCurrent.hitWindowGreat * 0.3),\n        );\n\n        windowPenalty = Math.min(1, windowPenalty);\n\n        let effectiveRatio = windowPenalty * currRatio;\n\n        if (firstDeltaSwitch) {\n          if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) {\n            if (islandSize < 7) islandSize++; // island is still progressing, count size.\n          } else {\n            if (isSlider(previousHitObject(j - 1)))\n              // bpm change is into slider, this is easy acc window\n              effectiveRatio *= 0.125;\n\n            if (isSlider(previousHitObject(j)))\n              // bpm change was from a slider, this is easier typically than circle -> circle\n              effectiveRatio *= 0.25;\n\n            if (previousIslandSize == islandSize)\n              // repeated island size (ex: triplet -> triplet)\n              effectiveRatio *= 0.25;\n\n            if (previousIslandSize % 2 == islandSize % 2)\n              // repeated island polartiy (2 -> 4, 3 -> 5)\n              effectiveRatio *= 0.5;\n\n            if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10)\n              // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.\n              effectiveRatio *= 0.125;\n\n            rhythmComplexitySum +=\n              (((Math.sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.sqrt(4 + islandSize)) / 2) *\n                Math.sqrt(4 + previousIslandSize)) /\n              2;\n\n            startRatio = effectiveRatio;\n\n            previousIslandSize = islandSize; // log the last island size.\n\n            if (prevDelta * 1.25 < currDelta)\n              // we're slowing down, stop counting\n              firstDeltaSwitch = false; // if we're speeding up, this stays true and  we keep counting island size.\n\n            islandSize = 1;\n          }\n        } else if (prevDelta > 1.25 * currDelta) {\n          // we want to be speeding up.\n          // Begin counting island until we change speed again.\n          firstDeltaSwitch = true;\n          startRatio = effectiveRatio;\n          islandSize = 1;\n        }\n      }\n\n      return Math.sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to\n      // strain. range [1, infinity) (not really\n      // though)\n    })();\n\n    currentStrain *= strainDecay(diffCurrent.strainTime);\n    currentStrain += speedEvaluateDifficultyOf * skillMultiplier;\n    const totalStrain = currentStrain * currentRhythm;\n    strains.push(totalStrain);\n\n    maxStrain = Math.max(maxStrain, totalStrain);\n    // This is a pretty expensive operation, therefore we need to optimize a bit\n    if (!onlyFinalRelevantCountValue || i + 1 === hitObjects.length) {\n      if (maxStrain <= 0 || strains.length == 0) {\n        relevantNoteCounts.push(0);\n      } else {\n        let sum = 0;\n        // We don't count the first artificial one ...\n        for (let j = 1; j < strains.length; ++j) {\n          sum += 1 / (1 + Math.exp(-((strains[j] / maxStrain) * 12.0 - 6.0)));\n        }\n        relevantNoteCounts.push(sum);\n      }\n    }\n  }\n  return { strains, relevantNoteCounts };\n}\n\nexport function calculateSpeed(hitObjects: OsuHitObject[], diffs: OsuDifficultyHitObject[], onlyFinalValue: boolean) {\n  // eslint-disable-next-line prefer-const\n  let { strains, relevantNoteCounts } = calculateSpeedInfo(hitObjects, diffs, onlyFinalValue);\n  return {\n    speedValues: calculateOsuStrainDifficultyValues(\n      diffs,\n      strains,\n      {\n        decayWeight: 0.9,\n        difficultyMultiplier: 1.04,\n        sectionDuration: 400,\n        reducedSectionCount: 5,\n        strainDecay,\n      },\n      onlyFinalValue,\n    ),\n    relevantNoteCounts,\n  };\n}\n"
  },
  {
    "path": "libs/osu/pp/src/lib/skills/strain.ts",
    "content": "import { clamp, lerp } from \"@osujs/math\";\nimport { OsuDifficultyHitObject } from \"../diff\";\nimport { sum } from \"simple-statistics\";\n\n// Not overridden ... yet?\nconst REDUCED_STRAIN_BASELINE = 0.75;\n\n// sectionDuration\n\ninterface StrainSkillParams {\n  sectionDuration: number;\n  strainDecay: (time: number) => number;\n}\n\ninterface OsuStrainSkillParams extends StrainSkillParams {\n  // 10 for aim and 5 in speed\n  reducedSectionCount: number;\n\n  // 0.9 except for 1.0 in FL\n  decayWeight: number;\n\n  difficultyMultiplier: number;\n}\n\nfunction calculateDifficultyValues(\n  diffs: OsuDifficultyHitObject[], // -> only startTime is used here\n  strains: number[],\n  { sectionDuration, strainDecay }: StrainSkillParams,\n  difficultyValueFromPeaks: (peaks: number[]) => number,\n  onlyFinalValue: boolean,\n): number[] {\n  if (diffs.length === 0) return [];\n  // osu!lazer note: sectionBegin = sectionDuration if t is dividable by sectionDuration (bug?)\n  const calcSectionBegin = (sectionDuration: number, t: number) => Math.floor(t / sectionDuration) * sectionDuration;\n\n  const peaks: number[] = [];\n  const difficultyValues: number[] = [];\n\n  let currentSectionBegin = calcSectionBegin(sectionDuration, diffs[0].startTime);\n  let currentSectionPeak = 0;\n\n  if (!onlyFinalValue) {\n    // For the first hitobject it is always 0\n    difficultyValues.push(0);\n  }\n\n  for (let i = 1; i < diffs.length; i++) {\n    const prevStartTime = diffs[i - 1].startTime;\n    const currStartTime = diffs[i].startTime;\n\n    // Let's see if we can close off the other sections\n    while (currentSectionBegin + sectionDuration < currStartTime) {\n      peaks.push(currentSectionPeak);\n      currentSectionBegin += sectionDuration;\n      currentSectionPeak = strains[i - 1] * strainDecay(currentSectionBegin - prevStartTime);\n    }\n\n    // Now check if the currentSectionPeak can be improved with the current hit object i\n    currentSectionPeak = Math.max(currentSectionPeak, strains[i]);\n\n    if (onlyFinalValue && i + 1 < diffs.length) {\n      continue;\n    }\n    // We do not push the currentSectionPeak to the peaks yet because currentSectionPeak is still in a jelly state and\n    // can be improved by the future hit objects in the same section.\n    const peaksWithCurrent = [...peaks, currentSectionPeak];\n    difficultyValues.push(difficultyValueFromPeaks(peaksWithCurrent));\n  }\n  return difficultyValues;\n}\n\nexport function calculateFlashlightDifficultyValues(\n  diffs: OsuDifficultyHitObject[],\n  strains: number[],\n  strainSkillParams: StrainSkillParams,\n  onlyFinalValue: boolean,\n): number[] {\n  // TODO: Dude this is making it n^2 ...\n  function difficultyValueFromPeaks(peaks: number[]) {\n    return sum(peaks) * 1.06;\n  }\n\n  return calculateDifficultyValues(diffs, strains, strainSkillParams, difficultyValueFromPeaks, onlyFinalValue);\n}\n\n/**\n * OsuStrainSkill\n * Summary of how the strain skill works:\n * - Strain is a value that decays exponentially over time if there is no hit object present\n * - Let strain at time t be S(t)\n *\n * - First the whole beatmap is partitioned into multiple sections each of duration D (D=400ms in osu!std) e.g. [0,\n * 400], [400, 800], ...\n * - Now we only consider the highest strain of each section aka \"section peak\" i.e. P(i) = max(S(t)) where i*D <= t <=\n * i*(D+1)\n * Note: This can be easily calculated since we know that the peak can only happen after each hit object or at the\n * beginning of a section\n *\n * - Finally the difficulty value of a strain skill considers the largest K strain peaks (K=10 in osu!std) and\n * nerfs them so that the extremly unique difficulty spikes get nerfed.\n *\n * - Then it uses the weighted sum to calculate the difficultyValue.\n *\n * Performance notes:\n * 1. O(n + D + D * log D) if only calculating the last value\n * 2. If we want to calculate for every value:\n *   This is O(n * D * log D) but can be optimized to O(n) by having a precision breakpoint\n *   -> For example, if we now want to push a peak that'd be the 150th highest value, then best it could get in\n *    is to become the 140th highest value -> its value multiplied with the weight 0.9^140 should be\n *    greater than some precision (let's say 10^-6), otherwise we just don't push it to the peaks. In theory, we should\n *    just be maintaining about ~100-150 peak values depending on the required precision which is O(1) compared to O(D).\n */\nexport function calculateOsuStrainDifficultyValues(\n  diffs: OsuDifficultyHitObject[],\n  strains: number[],\n  { reducedSectionCount, difficultyMultiplier, decayWeight, ...strainParams }: OsuStrainSkillParams,\n  onlyFinalValue: boolean,\n): number[] {\n  // OsuStrainSkill#DifficultyValue()\n  const descending = (a: number, b: number) => b - a;\n\n  function difficultyValueFromPeaks(peaks: number[]) {\n    // We do not push the currentSectionPeak to the peaks yet because currentSectionPeak is still in a jelly state and\n    // can be improved by the future hit objects in the same section.\n    peaks.sort(descending);\n    // This is now part of DifficultyValue()\n    for (let i = 0; i < Math.min(peaks.length, reducedSectionCount); i++) {\n      // Scale might be precalculated since it uses some expensive operation (log10)\n      const scale = Math.log10(lerp(1, 10, clamp(i / reducedSectionCount, 0, 1)));\n      peaks[i] *= lerp(REDUCED_STRAIN_BASELINE, 1.0, scale);\n    }\n    let weight = 1;\n\n    peaks = peaks.filter((p) => p > 0);\n    // Decreasingly\n    peaks.sort(descending);\n    let difficultyValue = 0;\n    for (const peak of peaks) {\n      difficultyValue += peak * weight;\n      weight *= decayWeight;\n    }\n    return difficultyValue * difficultyMultiplier;\n  }\n\n  return calculateDifficultyValues(diffs, strains, strainParams, difficultyValueFromPeaks, onlyFinalValue);\n}\n"
  },
  {
    "path": "libs/osu/pp/src/lib/utils.spec.ts",
    "content": "import { insertDecreasing } from \"./utils\";\n\ndescribe(\"insertDecreasing\", function () {\n  it(\"empty\", function () {\n    const q = [];\n    insertDecreasing(q, 3, 10);\n    expect(q).toEqual([3]);\n  });\n  it(\"inserting a new one\", function () {\n    const q = [4, 3, 2, 1];\n    insertDecreasing(q, 5, 5);\n    expect(q).toEqual([5, 4, 3, 2, 1]);\n  });\n  it(\"normal case\", function () {\n    const q = [5, 4, 3, 2, 1];\n    insertDecreasing(q, 3, 5);\n    expect(q).toEqual([5, 4, 3, 3, 2]);\n  });\n});\n"
  },
  {
    "path": "libs/osu/pp/src/lib/utils.ts",
    "content": "/**\n * Intended for small number of elements.\n *\n * This has a O(n) run time, but should be faster than a O(log n) heap implementation due to constant values for small\n * values of n.\n * @param queue will be mutated\n * @param element\n * @param maxSize\n */\nexport function insertDecreasing(queue: number[], element: number, maxSize: number) {\n  const n = queue.length;\n  let i, j;\n  for (i = 0; i < n; i++) {\n    if (queue[i] < element) {\n      break;\n    }\n  }\n\n  if (i === n) {\n    if (n < maxSize) {\n      queue.push(element);\n    }\n    return;\n  }\n\n  if (n < maxSize) queue.push(queue[n - 1]);\n  // Shift the elements after i to the right\n  for (j = n - 1; j > i; j--) queue[j] = queue[j - 1];\n  // \"Insert\" it here\n  queue[i] = element;\n  return queue;\n}\n"
  },
  {
    "path": "libs/osu/pp/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/pp/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu/pp/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.test.ts\",\n    \"**/*.spec.ts\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.js\",\n    \"**/*.spec.js\",\n    \"**/*.test.jsx\",\n    \"**/*.spec.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu/skin/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu/skin/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/skin/README.md",
    "content": "A small module to parse the `skin.ini` file.\n"
  },
  {
    "path": "libs/osu/skin/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-skin\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu/skin\",\n};\n"
  },
  {
    "path": "libs/osu/skin/src/index.ts",
    "content": "export * from \"./lib/SkinConfig\";\nexport * from \"./lib/SkinConfigParser\";\nexport * from \"./lib/TextureTypes\";\nexport * from \"./lib/OsuSkinTextureConfig\";\n"
  },
  {
    "path": "libs/osu/skin/src/lib/OsuSkinTextureConfig.ts",
    "content": "import { OsuSkinTextures } from \"./TextureTypes\";\n\nexport type OsuSkinTextureConfig = {\n  filePrefix: string;\n  animationFrameRate?: number;\n  skipHyphen?: boolean; // Most of the animated textures use hyphen '-' , but some do not such as sliderb0.png.\n};\n\nexport const TO_BE_DECIDED_BY_SKIN_INI = -1;\nconst defaultNumberConfig = (digit: number): OsuSkinTextureConfig => ({\n  filePrefix: `default-${digit}`,\n  // No animation possible\n});\nconst defaultScoreConfig = (digit: number): OsuSkinTextureConfig => ({\n  filePrefix: `score-${digit}`,\n  // No animation possible\n});\n\n// https://osu.ppy.sh/wiki/en/Skinning/Interface (normal)\n// https://osu.ppy.sh/wiki/el/Skinning/osu%21 (osu!std)\nexport const DEFAULT_SKIN_TEXTURE_CONFIG: Record<OsuSkinTextures, OsuSkinTextureConfig> = Object.freeze({\n  HIT_CIRCLE: {\n    filePrefix: \"hitcircle\",\n  },\n  APPROACH_CIRCLE: {\n    filePrefix: \"approachcircle\",\n  },\n  HIT_CIRCLE_OVERLAY: {\n    filePrefix: \"hitcircleoverlay\",\n    animationFrameRate: 2, // max 4FPS (see notes)\n  },\n  SLIDER_BALL: {\n    filePrefix: \"sliderb\",\n    // In McOsu/OsuSkin.cpp: m_sliderb->setAnimationFramerate(/*45.0f*/ 50.0f);\n    animationFrameRate: 50,\n    skipHyphen: true,\n  },\n  SLIDER_REPEAT: {\n    filePrefix: \"reversearrow\",\n  },\n  SLIDER_TICK: {\n    filePrefix: \"sliderscorepoint\",\n  },\n  SLIDER_FOLLOW_CIRCLE: {\n    filePrefix: \"sliderfollowcircle\",\n    animationFrameRate: TO_BE_DECIDED_BY_SKIN_INI,\n  },\n\n  DEFAULT_0: defaultNumberConfig(0),\n  DEFAULT_1: defaultNumberConfig(1),\n  DEFAULT_2: defaultNumberConfig(2),\n  DEFAULT_3: defaultNumberConfig(3),\n  DEFAULT_4: defaultNumberConfig(4),\n  DEFAULT_5: defaultNumberConfig(5),\n  DEFAULT_6: defaultNumberConfig(6),\n  DEFAULT_7: defaultNumberConfig(7),\n  DEFAULT_8: defaultNumberConfig(8),\n  DEFAULT_9: defaultNumberConfig(9),\n\n  COMBO_0: defaultScoreConfig(0),\n  COMBO_1: defaultScoreConfig(1),\n  COMBO_2: defaultScoreConfig(2),\n  COMBO_3: defaultScoreConfig(3),\n  COMBO_4: defaultScoreConfig(4),\n  COMBO_5: defaultScoreConfig(5),\n  COMBO_6: defaultScoreConfig(6),\n  COMBO_7: defaultScoreConfig(7),\n  COMBO_8: defaultScoreConfig(8),\n  COMBO_9: defaultScoreConfig(9),\n\n  // Technically speaking it is defined by the skin.ini config.\n  HIT_CIRCLE_0: defaultNumberConfig(0),\n  HIT_CIRCLE_1: defaultNumberConfig(1),\n  HIT_CIRCLE_2: defaultNumberConfig(2),\n  HIT_CIRCLE_3: defaultNumberConfig(3),\n  HIT_CIRCLE_4: defaultNumberConfig(4),\n  HIT_CIRCLE_5: defaultNumberConfig(5),\n  HIT_CIRCLE_6: defaultNumberConfig(6),\n  HIT_CIRCLE_7: defaultNumberConfig(7),\n  HIT_CIRCLE_8: defaultNumberConfig(8),\n  HIT_CIRCLE_9: defaultNumberConfig(9),\n\n  HIT_0: {\n    filePrefix: \"hit0\",\n    animationFrameRate: 60,\n  },\n  HIT_50: {\n    filePrefix: \"hit50\",\n    animationFrameRate: 60,\n  },\n  HIT_100: {\n    filePrefix: \"hit100\",\n    animationFrameRate: 60,\n  },\n  HIT_100K: {\n    filePrefix: \"hit100k\",\n    animationFrameRate: 60,\n  },\n  HIT_300: {\n    filePrefix: \"hit300\",\n    animationFrameRate: 60,\n  },\n  HIT_300K: {\n    filePrefix: \"hit300k\",\n    animationFrameRate: 60,\n  },\n  CURSOR: {\n    filePrefix: \"cursor\",\n  },\n  CURSOR_TRAIL: {\n    filePrefix: \"cursortrail\",\n  },\n\n  // TODO: Some spinner elements have restricted versions\n\n  SPINNER_APPROACH_CIRCLE: {\n    // Element is positioned around 397px vertically\n    // Applied to old and new style\n    filePrefix: \"spinner-approachcircle\",\n  },\n  SPINNER_RPM: {\n    /*\n    Origin: TopLeft\n    This element is positioned at 139px to the left from the middle of the screen and at 712px height\n(373,712) at 1024x768\n(544,712) at 1366x768\n     */\n    filePrefix: \"spinner-rpm\",\n  },\n\n  SPINNER_CLEAR: {\n    // Position around 230px vertically\n    // Shown, when player has fulfilled the spinner\n    filePrefix: \"spinner-clear\",\n  },\n  SPINNER_SPIN: {\n    // Positioned around 582px vertically\n    // Appears at the start of the spinner\n    filePrefix: \"spinner-spin\",\n  },\n  SPINNER_GLOW: {\n    filePrefix: \"spinner-glow\",\n  },\n  SPINNER_BOTTOM: {\n    filePrefix: \"spinner-bottom\",\n  },\n  SPINNER_TOP: {\n    filePrefix: \"spinner-top\",\n  },\n  SPINNER_MIDDLE2: {\n    filePrefix: \"spinner-middle2\",\n  },\n  SPINNER_MIDDLE: {\n    filePrefix: \"spinner-middle\",\n  },\n  SPINNER_OSU: {\n    filePrefix: \"spinner-osu\",\n  },\n  SPINNER_BACKGROUND: {\n    filePrefix: \"spinner-background\",\n  },\n  SPINNER_CIRCLE: {\n    filePrefix: \"spinner-circle\",\n  },\n  SCORE_X: {\n    filePrefix: \"score-x\",\n  },\n  SCORE_PERCENT: {\n    filePrefix: \"score-percent\",\n  },\n  SCORE_DOT: {\n    filePrefix: \"score-dot\",\n  },\n});\n"
  },
  {
    "path": "libs/osu/skin/src/lib/SkinConfig.ts",
    "content": "// https://osu.ppy.sh/community/forums/topics/533940\n\n// https://osu.ppy.sh/wiki/el/Skinning/skin.ini\n// Either RGB or RGBA\nexport type RGB = [number, number, number];\nexport type RGBA = [number, number, number, number];\nexport type Color = RGB | RGBA;\n\nexport type SkinConfig = {\n  general: {\n    name: string;\n    author: string;\n    version: string;\n    animationFrameRate: number | undefined;\n    allowSliderBallTint: boolean;\n    comboBurstRandom: boolean;\n    cursorCenter: boolean;\n    cursorExpand: boolean;\n    cursorRotate: boolean;\n    cursorTrailRotate: boolean;\n    customComboBurstSounds: number[];\n    hitCircleOverlayAboveNumber: boolean;\n    layeredHitSounds: boolean;\n    sliderBallFlip: boolean;\n    spinnerFadePlayfield: boolean;\n    spinnerFrequencyModulate: boolean;\n    spinnerNoBlink: boolean;\n    // sliderStyle is mentioned somewhere, but IDK what it stands for\n  };\n  // Technically speaking it's only RGB (but we also allow RGBA)\n  colors: {\n    comboColors: Color[];\n    inputOverlayText: Color;\n    menuGlow: Color;\n    sliderBall: Color;\n    sliderBorder: Color;\n    sliderTrackOverride: Color | undefined;\n    songSelectActiveText: Color;\n    songSelectInactiveText: Color;\n    spinnerBackground: Color;\n    starBreakAdditive: Color;\n  };\n  fonts: {\n    // Regarding overlap: Negative integers add a gap\n    hitCirclePrefix: string;\n    hitCircleOverlap: number;\n    scorePrefix: string;\n    scoreOverlap: number;\n    comboPrefix: string;\n    comboOverlap: number;\n  };\n  // CTB and Mania not supported\n};\n\nexport enum SkinIniSection {\n  NONE,\n  GENERAL,\n  COLORS,\n  FONTS,\n}\n\n// Using the default settings from https://osu.ppy.sh/wiki/el/Skinning/skin.ini\nexport const generateDefaultSkinConfig = (didFileExist: boolean): SkinConfig => ({\n  general: {\n    name: \"\",\n    author: \"\",\n    // If skin.ini is not present, `latest` will be used\n    // Otherwise 1.0 is assumed if version wasn't specified\n    version: didFileExist ? \"1.0\" : \"latest\",\n    animationFrameRate: undefined,\n    allowSliderBallTint: false,\n    comboBurstRandom: false,\n    cursorCenter: true, // anchor = center\n    cursorExpand: true,\n    cursorRotate: true,\n    cursorTrailRotate: true,\n    customComboBurstSounds: [],\n    hitCircleOverlayAboveNumber: true,\n    layeredHitSounds: true,\n    sliderBallFlip: true,\n    spinnerFadePlayfield: false,\n    spinnerFrequencyModulate: true,\n    spinnerNoBlink: false,\n  },\n  colors: {\n    comboColors: [\n      [255, 192, 0],\n      [0, 202, 0],\n      [18, 124, 255],\n      [242, 24, 57],\n    ],\n    inputOverlayText: [0, 0, 0],\n    menuGlow: [0, 78, 155],\n    sliderBall: [2, 170, 255],\n    sliderBorder: [255, 255, 255],\n    sliderTrackOverride: undefined, // this means to \"use current combo color\"\n    songSelectActiveText: [0, 0, 0],\n    songSelectInactiveText: [255, 255, 255],\n    spinnerBackground: [100, 100, 100],\n    starBreakAdditive: [255, 182, 193],\n  },\n  fonts: {\n    hitCirclePrefix: \"default\",\n    hitCircleOverlap: -2,\n    scorePrefix: \"score\",\n    scoreOverlap: -2,\n    comboPrefix: \"score\",\n    comboOverlap: -2,\n  },\n});\n"
  },
  {
    "path": "libs/osu/skin/src/lib/SkinConfigParser.spec.ts",
    "content": "import { parseSkinIni } from \"./SkinConfigParser\";\n\ntest(\"WhiteCat1.0/skin.ini parsing correctly\", function () {\n  // Source: WhiteCat 2.1 old lite\n  const data = `\n[General]\n  //----------General\n    Name: -        # WhiteCat (1.0) 『CK』 #-\n    Author: cyperdark\n    Version: 2.5\n\n  //----------Settings\n    AnimationFramerate: 60\n    AllowSliderBallTint: 1\n    ComboBurstRandom: 0\n    HitCircleOverlayAboveNumer: 0\n    SliderBallFlip: 1\n    SliderStyle: 2\n\n  //----------Cursor\n    CursorExpand: 0\n    CursorCentre: 1\n    CursorRotate: 0\n    CursorTrailRotate: 0\n\n[Colours]\n  //----------Combo colors\n    Combo1: 198, 173, 159 // #C6AD9F\n    Combo2: 150, 139, 136  // #968B88\n\n  //----------Text colors\n    InputOverlayText: 78, 70, 67 // #FFFFFF\n    SongSelectActiveText: 255, 255, 255 // #FFFFFF\n    SongSelectInactiveText: 200, 200, 200 // #C8C800\n\n  //----------Menu lines color\n    MenuGlow: 83, 127, 214 // #537FD6\n\n  //----------Spinner\n    SpinnerBackground: 255, 255, 255 // #FFFFFF\n\n  //----------Slider\n    //SliderBorder: 150, 139, 136 // #C6AD9F  #B29C8F\n    //SliderTrackOverride: 26, 24, 23 // #211813  #1A130F  #140F0C  #1A1817\n    //SliderBorder: 119, 110, 108 // #C6AD9F  #B29C8F  #757D5B  #968B88\n    //SliderTrackOverride: 35, 33, 32 // #211813  #1A130F  #140F0C  #1A1817  #1E2113  #2C2928\n    SliderBorder: 80, 80, 80\n    SliderTrackOverride: 0, 0, 0\n\n[Fonts]\n  //----------Hitcircle font\n    HitCirclePrefix: Assets/default/default\n    HitCircleOverlap: 15\n\n  //----------Score font\n    ScorePrefix: Assets/score/score\n    ScoreOverlap: 10\n\n  //----------Combo font\n    ComboPrefix: Assets/combo/combo\n    ComboOverlap: 10\n  `;\n  const skinIni = parseSkinIni(data);\n  const { general, colors, fonts } = skinIni;\n  // Just checking the most important ones ...\n\n  // General\n  expect(general.name).toEqual(\"-        # WhiteCat (1.0) 『CK』 #-\");\n  expect(general.author).toEqual(\"cyperdark\");\n  expect(general.version).toEqual(\"2.5\");\n  expect(general.animationFrameRate).toEqual(60);\n  expect(general.allowSliderBallTint).toEqual(true);\n\n  // Colors\n  expect(colors.comboColors).toEqual([\n    [198, 173, 159],\n    [150, 139, 136],\n  ]);\n  expect(colors.sliderBorder).toEqual([80, 80, 80]);\n\n  // Fonts\n  expect(fonts.hitCirclePrefix).toEqual(\"Assets/default/default\");\n  expect(fonts.hitCircleOverlap).toEqual(15);\n  expect(fonts.scoreOverlap).toEqual(10);\n  expect(fonts.scorePrefix).toEqual(\"Assets/score/score\");\n  expect(fonts.comboOverlap).toEqual(10);\n  expect(fonts.comboPrefix).toEqual(\"Assets/combo/combo\");\n});\n\ntest(\"MillhioreLite/skin.ini parsing - missing some values\", function () {\n  const data = `\n[General]\nName: Millhiore Lite\nAuthor: MillhioreF\n\nCursorExpand: 1\nSliderBallFrames: 2\nSliderBallFlip: 0\nVersion: latest\n\n[Colours]\nSliderTrackOverride:0,0,0\n[Mania]\nKeys: 4\n//Mania skin config\nColumnStart: 136\nHitPosition: 402\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 1\nScorePosition: 325\nComboPosition: 111\nLightFramePerSecond: 24\nColumnWidth: 30,30,30,30\n//Colours\n//images\n//Keys\n[Mania]\nKeys: 5\n//Mania skin config\nColumnStart: 136\nHitPosition: 402\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 1\nScorePosition: 325\nComboPosition: 111\nLightFramePerSecond: 24\nColumnWidth: 30,30,30,30,30\n//Colours\n//images\n//Keys\n[Mania]\nKeys: 6\n//Mania skin config\nColumnStart: 136\nHitPosition: 402\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 1\nScorePosition: 325\nComboPosition: 111\nLightFramePerSecond: 24\nColumnWidth: 30,30,30,30,30,30\n//Colours\n//images\n//Keys\n[Mania]\nKeys: 7\n//Mania skin config\nColumnStart: 136\nHitPosition: 402\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 1\nScorePosition: 325\nComboPosition: 111\nLightFramePerSecond: 24\nColumnWidth: 30,30,30,30,30,30,30\n//Colours\n//images\n//Keys\n[Mania]\nKeys: 8\n//Mania skin config\nColumnStart: 136\nHitPosition: 402\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 1\nScorePosition: 325\nComboPosition: 111\nLightFramePerSecond: 24\nColumnWidth: 30,30,30,30,30,30,30,30\n//Colours\n//images\n//Keys\n  `;\n\n  const skinIni = parseSkinIni(data);\n  const { general, colors } = skinIni;\n\n  expect(general.name).toEqual(\"Millhiore Lite\");\n\n  // The colors do not exist so it's just using the default values\n  expect(colors.comboColors).toEqual([\n    [255, 192, 0],\n    [0, 202, 0],\n    [18, 124, 255],\n    [242, 24, 57],\n  ]);\n});\n"
  },
  {
    "path": "libs/osu/skin/src/lib/SkinConfigParser.ts",
    "content": "import { Color, generateDefaultSkinConfig, SkinConfig, SkinIniSection } from \"./SkinConfig\";\n\nfunction parseToBool(val: string): boolean {\n  const v = parseInt(val);\n  return v !== 0;\n}\n\n// TODO: No need for classes, need refactoring\nclass SkinConfigParser {\n  data: string;\n  skinConfig: SkinConfig;\n  section: SkinIniSection;\n\n  comboColorsAdded = false;\n\n  constructor(data: string) {\n    this.data = data;\n    this.skinConfig = generateDefaultSkinConfig(true);\n    this.section = SkinIniSection.NONE;\n  }\n\n  shouldSkipLine(line: string): boolean {\n    if (!line) return true;\n    return line.trim().startsWith(\"//\");\n  }\n\n  stripComments(line: string): string {\n    const index = line.indexOf(\"//\");\n    if (index > 0) {\n      return line.substring(0, index);\n    }\n    return line;\n  }\n\n  splitKeyVal(line: string, separator = \":\"): [string, string] {\n    let split = line.split(separator, 2);\n    if (split.length < 2) split.push(\"\");\n    split = split.map((s) => s.trim());\n    return [split[0], split[1]];\n  }\n\n  handleGeneral(key: string, val: string) {\n    const { general } = this.skinConfig;\n    switch (key) {\n      case \"Name\":\n        general.name = val;\n        break;\n      case \"Author\":\n        general.author = val;\n        break;\n      case \"Version\":\n        // Maybe check for 1.0, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, latest ?\n        general.version = val;\n        break;\n      case \"AnimationFramerate\":\n        general.animationFrameRate = parseInt(val);\n        break;\n      case \"AllowSliderBallTint\":\n        general.allowSliderBallTint = parseToBool(val);\n        break;\n      case \"ComboBurstRandom\":\n        general.comboBurstRandom = parseToBool(val);\n        break;\n      case \"CursorCentre\": // yes spelled differently\n        general.cursorCenter = parseToBool(val);\n        break;\n      case \"CursorExpand\":\n        general.cursorExpand = parseToBool(val);\n        break;\n      case \"CursorRotate\":\n        general.cursorRotate = parseToBool(val);\n        break;\n      case \"CursorTrailRotate\":\n        general.cursorTrailRotate = parseToBool(val);\n        break;\n      case \"CustomComboBurstSounds\":\n        // ??? no clue\n        general.customComboBurstSounds = val.split(\",\").map((s) => parseInt(s));\n        break;\n      case \"HitCircleOverlayAboveNumber\":\n      case \"HitCircleOverlayAboveNumer\": // typo also supported\n        general.hitCircleOverlayAboveNumber = parseToBool(val);\n        break;\n\n      case \"LayeredHitSounds\":\n        general.layeredHitSounds = parseToBool(val);\n        break;\n      case \"SliderBallFlip\":\n        general.sliderBallFlip = parseToBool(val);\n        break;\n      case \"SpinnerFadePlayfield\":\n        general.spinnerFadePlayfield = parseToBool(val);\n        break;\n      case \"SpinnerFrequencyModulate\":\n        general.spinnerFrequencyModulate = parseToBool(val);\n        break;\n      case \"SpinnerNoBlink\":\n        general.spinnerNoBlink = parseToBool(val);\n        break;\n      default:\n      // console.log(`key=${key} not recognized`);\n    }\n  }\n\n  handleFonts(key: string, val: string) {\n    const { fonts } = this.skinConfig;\n    switch (key) {\n      case \"HitCirclePrefix\":\n        fonts.hitCirclePrefix = val;\n        break;\n      case \"HitCircleOverlap\":\n        fonts.hitCircleOverlap = parseInt(val);\n        break;\n      case \"ScorePrefix\":\n        fonts.scorePrefix = val;\n        break;\n      case \"ScoreOverlap\":\n        fonts.scoreOverlap = parseInt(val);\n        break;\n      case \"ComboPrefix\":\n        fonts.comboPrefix = val;\n        break;\n      case \"ComboOverlap\":\n        fonts.comboOverlap = parseInt(val);\n        break;\n    }\n  }\n\n  handleColors(key: string, val: string) {\n    const numbers = val.split(\",\").map((s) => parseInt(s));\n    if (numbers.length !== 3 && numbers.length !== 4) {\n      console.error(\"Should provide (R,G,B) or (R,G,B,A)\");\n      return;\n    }\n\n    const { colors } = this.skinConfig;\n\n    const color = numbers as Color;\n\n    switch (key) {\n      case \"Combo0\":\n      case \"Combo1\":\n      case \"Combo2\":\n      case \"Combo3\":\n      case \"Combo4\":\n      case \"Combo5\":\n      case \"Combo6\":\n      case \"Combo7\":\n      case \"Combo8\":\n      case \"Combo9\":\n        // There is a default combo color list\n        if (this.comboColorsAdded) colors.comboColors.push(color);\n        else {\n          colors.comboColors = [color];\n          this.comboColorsAdded = true;\n        }\n        break;\n      case \"InputOverlayText\":\n        colors.inputOverlayText = color;\n        break;\n      case \"MenuGlow\":\n        colors.menuGlow = color;\n        break;\n      case \"SliderBall\":\n        colors.sliderBall = color;\n        break;\n      case \"SliderBorder\":\n        colors.sliderBorder = color;\n        break;\n      case \"SliderTrackOverride\":\n        colors.sliderTrackOverride = color;\n        break;\n      case \"SongSelectActiveText\":\n        colors.songSelectActiveText = color;\n        break;\n      case \"SongSelectInactiveText\":\n        colors.songSelectInactiveText = color;\n        break;\n      case \"SpinnerBackground\":\n        colors.spinnerBackground = color;\n        break;\n      case \"StarBreakAdditive\":\n        colors.starBreakAdditive = color;\n        break;\n    }\n  }\n\n  getSection(s: string) {\n    switch (s) {\n      case \"General\":\n        return SkinIniSection.GENERAL;\n      case \"Colours\":\n        return SkinIniSection.COLORS;\n      case \"Fonts\":\n        return SkinIniSection.FONTS;\n    }\n    return SkinIniSection.NONE;\n  }\n\n  parseLine(line: string): void {\n    if (this.shouldSkipLine(line)) return;\n    if (line.startsWith(\"[\") && line.endsWith(\"]\")) {\n      const sectionStr = line.substring(1, line.length - 1);\n      this.section = this.getSection(sectionStr);\n      return;\n    }\n    line = this.stripComments(line);\n    const [key, val] = this.splitKeyVal(line);\n    switch (this.section) {\n      case SkinIniSection.GENERAL:\n        this.handleGeneral(key, val);\n        break;\n      case SkinIniSection.COLORS:\n        this.handleColors(key, val);\n        break;\n      case SkinIniSection.FONTS:\n        this.handleFonts(key, val);\n        break;\n      default:\n      // console.log(\"Line was ignored cause not in a section right now\");\n    }\n  }\n\n  parse(): SkinConfig {\n    if (!this.data) throw Error(\"No data given\");\n    const lines = this.data.split(/\\r?\\n/).map((v) => v.trim());\n    for (const line of lines) {\n      this.parseLine(line);\n    }\n    return this.skinConfig;\n  }\n}\n\nexport function parseSkinIni(data: string): SkinConfig {\n  return new SkinConfigParser(data).parse();\n}\n"
  },
  {
    "path": "libs/osu/skin/src/lib/TextureTypes.ts",
    "content": "// Would have been cleaner, but iterating over the arrays is also important ;(\n// https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html\n// const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] as const;\n// type Digit = typeof digits[number];\n// type DefaultDigitFont = `DEFAULT_${Digit}`;\n// type ScoreDigitFont = `SCORE_${Digit}`;\n// type ComboDigitFont = `COMBO_${Digit}`;\n// type HitCircleDigitFont = `HIT_CIRCLE_${Digit}`;\n\nexport const defaultDigitFonts = [\n  \"DEFAULT_0\",\n  \"DEFAULT_1\",\n  \"DEFAULT_2\",\n  \"DEFAULT_3\",\n  \"DEFAULT_4\",\n  \"DEFAULT_5\",\n  \"DEFAULT_6\",\n  \"DEFAULT_7\",\n  \"DEFAULT_8\",\n  \"DEFAULT_9\",\n] as const;\ntype DefaultDigitFont = typeof defaultDigitFonts[number];\nexport const comboDigitFonts = [\n  \"COMBO_0\",\n  \"COMBO_1\",\n  \"COMBO_2\",\n  \"COMBO_3\",\n  \"COMBO_4\",\n  \"COMBO_5\",\n  \"COMBO_6\",\n  \"COMBO_7\",\n  \"COMBO_8\",\n  \"COMBO_9\",\n] as const;\ntype ComboDigitFont = typeof comboDigitFonts[number];\nexport const hitCircleDigitFonts = [\n  \"HIT_CIRCLE_0\",\n  \"HIT_CIRCLE_1\",\n  \"HIT_CIRCLE_2\",\n  \"HIT_CIRCLE_3\",\n  \"HIT_CIRCLE_4\",\n  \"HIT_CIRCLE_5\",\n  \"HIT_CIRCLE_6\",\n  \"HIT_CIRCLE_7\",\n  \"HIT_CIRCLE_8\",\n  \"HIT_CIRCLE_9\",\n] as const;\ntype HitCircleDigitFont = typeof hitCircleDigitFonts[number];\n\ntype HitCircleTextures = \"APPROACH_CIRCLE\" | \"HIT_CIRCLE\" | \"HIT_CIRCLE_OVERLAY\";\ntype SliderTextures = \"SLIDER_BALL\" | \"SLIDER_FOLLOW_CIRCLE\" | \"SLIDER_TICK\" | \"SLIDER_REPEAT\";\ntype SpinnerTextures =\n  | \"SPINNER_APPROACH_CIRCLE\"\n  | \"SPINNER_BACKGROUND\"\n  | \"SPINNER_BOTTOM\"\n  | \"SPINNER_CIRCLE\"\n  | \"SPINNER_CLEAR\"\n  | \"SPINNER_GLOW\"\n  | \"SPINNER_MIDDLE2\"\n  | \"SPINNER_MIDDLE\"\n  | \"SPINNER_OSU\"\n  | \"SPINNER_RPM\"\n  | \"SPINNER_SPIN\"\n  | \"SPINNER_TOP\";\ntype JudgementTextures = \"HIT_0\" | \"HIT_50\" | \"HIT_100\" | \"HIT_100K\" | \"HIT_300\" | \"HIT_300K\";\ntype CursorTextures = \"CURSOR\" | \"CURSOR_TRAIL\";\ntype ScoreTextures = \"SCORE_X\" | \"SCORE_DOT\" | \"SCORE_PERCENT\";\nexport type OsuSkinTextures =\n  | HitCircleTextures\n  | SliderTextures\n  | SpinnerTextures\n  | JudgementTextures\n  | CursorTextures\n  | DefaultDigitFont\n  // | ScoreDigitFont\n  | ComboDigitFont\n  | HitCircleDigitFont\n  | ScoreTextures;\n\n// export const isHitCircleFont = (h: OsuSkinTextures) : h is HitCircleDigitFont =>\n"
  },
  {
    "path": "libs/osu/skin/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu/skin/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": []\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu/skin/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/README.md",
    "content": "# osu-local-db-reader\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test osu-local-db-reader` to execute the unit tests via [Jest](https://jestjs.io).\n"
  },
  {
    "path": "libs/osu-local/db-reader/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-local-db-reader\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu-local/db-reader\",\n};\n"
  },
  {
    "path": "libs/osu-local/db-reader/src/DatabaseReader.ts",
    "content": "import { OsuBuffer } from \"./OsuBuffer\";\n\n// .db and .osr files can be read with this\nexport class Reader {\n  protected readonly buffer: OsuBuffer;\n\n  constructor(buffer: Buffer) {\n    this.buffer = new OsuBuffer(buffer);\n  }\n\n  readByte() {\n    return this.buffer.readByte();\n  }\n\n  readShort() {\n    return this.buffer.readUInt16();\n  }\n\n  readInt() {\n    return this.buffer.readUInt32();\n  }\n\n  readLong() {\n    return this.buffer.readUInt64();\n  }\n\n  readString() {\n    return this.buffer.readOsuString();\n  }\n\n  readSingle() {\n    return this.buffer.readFloat();\n  }\n\n  readDouble() {\n    return this.buffer.readDouble();\n  }\n\n  readBoolean() {\n    return this.buffer.readBoolean();\n  }\n\n  readDateTime() {\n    return this.buffer.readUInt64();\n  }\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/src/DatabaseTypes.ts",
    "content": "/**\n * https://osu.ppy.sh/wiki/cs/osu!_File_Formats/Db_(file_format)\n To ease the description of the format of each .db file, the following names\n for data types will be used. Unless otherwise specified, all numerical types\n are stored little-endian. Integer values, including bytes, are all unsigned.\n UTF-8 characters are stored in their canonical form, with the higher-order byte first.\n */\ntype Byte = number;\ntype Short = number;\ntype Int = number;\ntype Long = bigint; // number can only represent up to 2^53\ntype ULEB128 = bigint; // hmm..\ntype Single = number; // 32bit IEEE floating point value\ntype Double = number; // 64bit IEEE floating point value\n\n// osu!.db specific\ntype IntSinglePair = [Int, Single];\nexport type TimingPoint = {\n  bpm: Double;\n  offset: Double;\n  inherited: boolean;\n};\n// \tA 64-bit number of ticks representing a date and time. Ticks are the amount of 100-nanosecond intervals since midnight, January 1, 0001 UTC. See .NET framework documentation on ticks for more information.\ntype DateTime = bigint;\n\nexport type OsuDB = {\n  osuVersion: Int;\n  folderCount: Int;\n  accountIsUnlocked: boolean;\n  accountUnlockDate: DateTime;\n  playerName: string;\n  numberOfBeatmaps: Int;\n  beatmaps: Beatmap[];\n  userPermissions: Int; // (0 = None, 1 = Normal, 2 = Moderator, 4 = Supporter, 8 = Friend, 16 = peppy, 32 = World Cup staff)\n};\n\nenum RankedStatus {\n  UNKNOWN = 0,\n  UNSUBMITTED = 1,\n  PENDING = 2,\n  UNUSED = 3,\n  RANKED = 4,\n  APPROVED = 5,\n  QUALIFIED = 6,\n  LOVED = 7,\n}\n\nenum GameplayMode {\n  STD = 0x00,\n  TAIKO = 0x01,\n  CTB = 0x02,\n  MANIA = 0x03,\n}\n\n// The first one will be the mods as a mask and the second one as the star rating\nexport type StarRatings = IntSinglePair[];\n\nexport type Beatmap = {\n  bytesOfBeatmapEntry: Int;\n  artist: string;\n  artistUnicode: string;\n  title: string;\n  titleUnicode: string;\n  creator: string;\n  difficulty: string; // e.g. Reform's Insane\n  audioFileName: string;\n  md5Hash: string;\n  fileName: string;\n  rankedStatus: RankedStatus;\n  circlesCount: Short;\n  slidersCount: Short;\n  spinnersCount: Short;\n  lastModifiedTime: DateTime;\n  approachRate: Single; // Byte if version <= 20140609\n  circleSize: Single;\n  hpDrain: Single;\n  overallDifficulty: Single;\n  sliderVelocity: Double;\n  stdStarRatings: StarRatings;\n  taikoStarRatings: StarRatings;\n  catchStarRatings: StarRatings;\n  maniaStarRatings: StarRatings;\n  drainTime: Int; // in seconds\n  totalTime: Int; // in milliseconds\n  audioPreviewTime: Int;\n  timingPoints: TimingPoint[];\n  beatmapId: Int;\n  beatmapSetId: Int;\n  threadId: Int;\n  stdGrade: Byte;\n  taikoGrade: Byte;\n  ctbGrade: Byte;\n  maniaGrade: Byte;\n  localOffset: Short;\n  stackLeniency: Single;\n  gameplayMode: GameplayMode;\n  source: string;\n  tags: string;\n  offset: Short;\n  titleFont: string;\n  isUnplayed: boolean;\n  lastPlayed: DateTime;\n  isOsz2: boolean;\n  folderName: string;\n  lastCheckedAgainstOsuRepo: DateTime;\n  ignoreBeatmapSound: boolean;\n  ignoreBeatmapSkin: boolean;\n  disableStoryboard: boolean;\n  disableVideo: boolean;\n  visualOverride: boolean;\n  maniaScrollSpeed: Byte;\n};\n\nexport type Score = {\n  gameplayMode: GameplayMode;\n  version: Int; // Int\n  beatmapMD5: string;\n  player: string;\n  replayMD5: string;\n  numberOf300s: Short; // Short\n  numberOf100s: Short; // Short\n  numberOf50s: Short; // Short\n  numberOfGekis: Short; // Short\n  numberOfKatus: Short; // Short\n  numberOfMisses: Short; // Short\n  replayScore: Int; // Int\n  maxCombo: Short; // Short\n  perfectCombo: boolean;\n  modsBitmask: Int; // Int\n  timestamp: DateTime; // Long; in Windows Ticks\n  onlineScoreId: Long; // Long\n  additionalModInfo: Double; // Double(only present if Target Practice is enabled)\n};\n\nexport type ScoresDBBeatmap = {\n  md5hash: string;\n  numberOfScores: Int; // Int\n  scores: Score[];\n};\n\nexport type ScoresDB = {\n  version: Int; // Int\n  numberOfBeatmaps: Int; // Int\n  beatmaps: ScoresDBBeatmap[];\n};\n"
  },
  {
    "path": "libs/osu-local/db-reader/src/OsuBuffer.ts",
    "content": "/**\n MIT License\n\n Copyright (c) 2017 Dillon Modine-Thuen\n\n Permission is hereby granted, free of charge, to any person obtaining a copy\n of this software and associated documentation files (the \"Software\"), to deal\n in the Software without restriction, including without limitation the rights\n to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n copies of the Software, and to permit persons to whom the Software is\n furnished to do so, subject to the following conditions:\n\n The above copyright notice and this permission notice shall be included in all\n copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n SOFTWARE.\n\n */\n\nexport class OsuBuffer {\n  buffer: Buffer;\n  position: number;\n\n  /**\n   * @param input\n   */\n  constructor(input?: Buffer) {\n    this.buffer = Buffer.from(input instanceof Buffer ? input : []);\n    this.position = 0;\n  }\n\n  /**\n   * Returns the full length of the buffer\n   * @returns {Number}\n   */\n  get length() {\n    return this.buffer.length;\n  }\n\n  /**\n   * Returns buffer to a binary string\n   * @returns {String}\n   */\n  toString(type: \"binary\" | \"ascii\" = \"binary\") {\n    return this.buffer.toString(type);\n  }\n\n  /**\n   * Creates a new OsuBuffer from arguments\n   * @returns {OsuBuffer}\n   */\n  static from(args: any) {\n    if (args[0] instanceof OsuBuffer) {\n      args[0] = args[0].buffer;\n    }\n    return new OsuBuffer(Buffer.from.apply(Buffer, args));\n  }\n\n  /**\n   * Returns boolean if can read from defined length from buffer\n   * @param {Number} length\n   * @return {boolean}\n   */\n  canRead(length: number) {\n    return length + this.position <= this.buffer.length;\n  }\n\n  /**\n   * Returns boolean if at end of the buffer\n   * @return {boolean}\n   */\n  isEOF() {\n    return this.position >= this.buffer.length;\n  }\n\n  /**\n   * Slices and returns buffer\n   * @param {Number} length\n   * @param {Boolean?} asOsuBuffer\n   * @return {OsuBuffer|Buffer}\n   */\n  slice(length: number, asOsuBuffer = true) {\n    this.position += length;\n    return asOsuBuffer\n      ? OsuBuffer.from(this.buffer.slice(this.position - length, this.position))\n      : this.buffer.slice(this.position - length, this.position);\n  }\n\n  // Reading\n\n  /**\n   * Peeks the next byte in the buffer without shifting the position\n   * @return {Number|undefined}\n   */\n  peek() {\n    return this.buffer[this.position + 1];\n  }\n\n  /**\n   * Reads a byte from the buffer\n   * Does the same thing as ReadUInt8()\n   * @return {Number}\n   */\n  readByte() {\n    return this.readUInt8();\n  }\n\n  /**\n   * Reads a signed integer from the Buffer\n   * @param {Number} byteLength\n   * @return {Number}\n   */\n  readInt(byteLength: number) {\n    this.position += byteLength;\n    return this.buffer.readIntLE(this.position - byteLength, byteLength);\n  }\n\n  /**\n   * Reads a unsigned integer from the Buffer\n   * @param {Number} byteLength\n   * @return {Number}\n   */\n  readUInt(byteLength: number) {\n    this.position += byteLength;\n    return this.buffer.readUIntLE(this.position - byteLength, byteLength);\n  }\n\n  /**\n   * Reads a 8-bit signed integer from the buffer\n   * @return {Number}\n   */\n  readInt8() {\n    return this.readInt(1);\n  }\n\n  /**\n   * Reads a 8-bit unsigned integer from the buffer\n   * @return {Number}\n   */\n  readUInt8() {\n    return this.readUInt(1);\n  }\n\n  /**\n   * Reads a 16-bit signed integer from the buffer\n   * @return {Number}\n   */\n  readInt16() {\n    return this.readInt(2);\n  }\n\n  /**\n   * Reads a 16-bit unsigned integer from the buffer\n   * @return {Number}\n   */\n  readUInt16() {\n    return this.readUInt(2);\n  }\n\n  /**\n   * Reads a 32-bit signed integer from the buffer\n   * @return {Number}\n   */\n  readInt32() {\n    return this.readInt(4);\n  }\n\n  /**\n   * Reads a 32-bit unsigned integer from the buffer\n   * @return {Number}\n   */\n  readUInt32() {\n    return this.readUInt(4);\n  }\n\n  /**\n   * Reads a 64-bit signed integer from the buffer\n   * @return {Number}\n   */\n  readInt64(): BigInt {\n    this.position += 8;\n    return this.buffer.readBigInt64LE(this.position - 8);\n  }\n\n  /**\n   * Reads a 64-bit unsigned integer from the buffer\n   * @return {Number}\n   */\n  readUInt64() {\n    this.position += 8;\n    return this.buffer.readBigUInt64LE(this.position - 8);\n  }\n\n  /**\n   * Reads a 32-bit Float from the buffer\n   * @returns {Number}\n   */\n  readFloat() {\n    this.position += 4;\n    return this.buffer.readFloatLE(this.position - 4);\n  }\n\n  /**\n   * Reads a 64-bit Double from the buffer\n   * @returns {Number}\n   */\n  readDouble() {\n    this.position += 8;\n    return this.buffer.readDoubleLE(this.position - 8);\n  }\n\n  /**\n   * Reads a string from the buffer\n   * @param {Number} length\n   * @returns {String}\n   */\n  readString(length: number) {\n    return this.slice(length, false).toString();\n  }\n\n  /**\n   * Decodes a 7-bit encoded integer from the buffer\n   * @returns {Number}\n   */\n  readVarInt() {\n    let total = 0;\n    let shift = 0;\n    let byte = this.readUInt8();\n    if ((byte & 0x80) === 0) {\n      total |= (byte & 0x7f) << shift;\n    } else {\n      let end = false;\n      do {\n        if (shift) {\n          byte = this.readUInt8();\n        }\n        total |= (byte & 0x7f) << shift;\n        if ((byte & 0x80) === 0) end = true;\n        shift += 7;\n      } while (!end);\n    }\n\n    return total;\n  }\n\n  /**\n   * Decodes a 7-bit encoded integer from the buffer\n   * @deprecated Use ReadVarint instead\n   * @returns {Number}\n   */\n  readULeb128() {\n    return this.readVarInt();\n  }\n\n  /**\n   * Reads a byte from buffer and converts to boolean\n   * @return {boolean}\n   */\n  readBoolean() {\n    return Boolean(this.readInt(1));\n  }\n\n  /**\n   * Reads an osu! encoded string from the Buffer\n   * @returns {string}\n   */\n  readOsuString() {\n    const isString = this.readByte() === 11;\n    if (isString) {\n      const len = this.readVarInt();\n      return this.readString(len);\n    } else {\n      return \"\";\n    }\n  }\n\n  // Writing\n\n  /**\n   * Concats a buffer to the current buffer\n   * @param {Buffer} value\n   * @return {OsuBuffer}\n   */\n  writeBuffer(value: Buffer): OsuBuffer {\n    this.buffer = Buffer.concat([this.buffer, value]);\n    return this;\n  }\n\n  /**\n   * Writes an unsinged integer of any byte length\n   * @param {Number} value\n   * @param {Number} byteLength\n   * @return {OsuBuffer}\n   */\n  writeUInt(value: number, byteLength: number): OsuBuffer {\n    const buff = Buffer.alloc(byteLength);\n    buff.writeUIntLE(value, 0, byteLength);\n\n    return this.writeBuffer(buff);\n  }\n\n  /**\n   * Writes an integer of any byte length\n   * @param {Number} value\n   * @param {Number} byteLength\n   * @return {OsuBuffer}\n   */\n  writeInt(value: number, byteLength: number) {\n    const buff = Buffer.alloc(byteLength);\n    buff.writeIntLE(value, 0, byteLength);\n\n    return this.writeBuffer(buff);\n  }\n\n  /**\n   * Writes a 8-bit integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeByte(value: number) {\n    return this.writeBuffer(Buffer.alloc(1, value));\n  }\n\n  /**\n   *\n   * @param {Array} value\n   * @return {OsuBuffer}\n   */\n  writeBytes(value: Array<any>) {\n    return this.writeBuffer(Buffer.from(value));\n  }\n\n  /**\n   * Writes a 8-bit integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeUInt8(value: number) {\n    return this.writeUInt(value, 1);\n  }\n\n  /**\n   * Writes a 8-bit integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeInt8(value: number) {\n    return this.writeInt(value, 1);\n  }\n\n  /**\n   * Writes a 16-bit unsigned integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeUInt16(value: number) {\n    return this.writeUInt(value, 2);\n  }\n\n  /**\n   * Writes a 16-bit signed integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeInt16(value: number) {\n    return this.writeInt(value, 2);\n  }\n\n  /**\n   * Writes a 32-bit unsigned integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeUInt32(value: number) {\n    return this.writeUInt(value, 4);\n  }\n\n  /**\n   * Writes a 32-bit signed integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeInt32(value: number) {\n    return this.writeInt(value, 4);\n  }\n\n  /**\n   * Writes a 64-bit unsigned integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   * // TODO: Not tested (there was a bug with read, so I would be careful about this)\n   */\n  writeUInt64(value: number) {\n    const buff = Buffer.alloc(8);\n    // High\n    buff.writeUInt32LE(value >> 8, 0);\n    // Low\n    buff.writeUInt32LE(value & 0x00ff, 4);\n\n    return this.writeBuffer(buff);\n  }\n\n  /**\n   * Writes a 64-bit signed integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeInt64(value: number) {\n    const buff = Buffer.alloc(8);\n    // High\n    buff.writeInt32LE(value >> 8, 0);\n    // Low\n    buff.writeInt32LE(value & 0x00ff, 4);\n\n    return this.writeBuffer(buff);\n  }\n\n  /**\n   * Writes a 32-bit float to the buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeFloat(value: number) {\n    const buff = Buffer.alloc(4);\n    buff.writeFloatLE(value, 0);\n\n    return this.writeBuffer(buff);\n  }\n\n  /**\n   * Writes a 64-bit double to the buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeDouble(value: number) {\n    const buff = Buffer.alloc(8);\n    buff.writeDoubleLE(value, 0);\n\n    return this.writeBuffer(buff);\n  }\n\n  /**\n   * Writes a string to the Buffer\n   * @param {string} value\n   * @return {OsuBuffer}\n   */\n  writeString(value: string) {\n    const buff = Buffer.alloc(Buffer.byteLength(value, \"utf8\"));\n    buff.write(value);\n\n    return this.writeBuffer(buff);\n  }\n\n  /**\n   * Writes a boolean to the buffer\n   * @param {boolean} value\n   * @return {OsuBuffer}\n   */\n  writeBoolean(value: number) {\n    return this.writeByte(value ? 1 : 0);\n  }\n\n  /**\n   * Writes an osu! encoded string to the Buffer\n   * @param {string?} value\n   * @param nullable\n   * @return {OsuBuffer}\n   */\n  writeOsuString(value: string, nullable = false) {\n    if (value.length === 0 && nullable) {\n      this.writeByte(0);\n    } else if (value.length === 0) {\n      this.writeByte(11);\n      this.writeByte(0);\n    } else {\n      this.writeByte(11);\n      this.writeVarInt(Buffer.byteLength(value, \"utf8\"));\n      this.writeString(value);\n    }\n    return this;\n  }\n\n  /**\n   * Writes an unsigned 7-bit encoded integer to the Buffer\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeVarInt(value: number) {\n    const arr: any[] = [];\n    let len = 0;\n    // TODO: Push is faster\n    do {\n      arr[len] = value & 0x7f;\n      if ((value >>= 7) !== 0) arr[len] |= 0x80;\n      len++;\n    } while (value > 0);\n\n    return this.writeBuffer(Buffer.from(arr));\n  }\n\n  /**\n   * Writes an unsigned 7-bit encoded integer to the Buffer\n   * @deprecated Use WriteUVarint instead\n   * @param {Number} value\n   * @return {OsuBuffer}\n   */\n  writeULeb128(value: number) {\n    return this.writeVarInt(value);\n  }\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/src/OsuDBReader.ts",
    "content": "import { Beatmap, OsuDB, StarRatings, TimingPoint } from \"./DatabaseTypes\";\nimport { Reader } from \"./DatabaseReader\";\n\n// Sources:\n// https://github.com/Piotrekol/CollectionManager/blob/cb870d363d593035c97dc65f316a93f2d882c98b/CollectionManagerDll/Modules/FileIO/OsuDb/OsuDatabaseReader.cs#L232\n// https://github.com/mrflashstudio/OsuParsers/blob/a0d9d18a079f83fd679b31c66d9f315d4b72ca9c/OsuParsers/Serialization/SerializationReader.cs#L52\nexport class OsuDBReader extends Reader {\n  readStarRatings(): StarRatings {\n    const buffer = this.buffer;\n    // Count can actually be negative\n    const count = buffer.readInt32();\n    const list: StarRatings = [];\n    for (let i = 0; i < count; i++) {\n      const b1 = this.readByte(); // === 0x08\n      const mods = buffer.readInt32();\n      const b2 = buffer.readByte(); // === 0x0c\n      const stars = buffer.readFloat();\n      if (mods !== undefined && stars !== undefined) list.push([mods, stars]);\n    }\n    return list;\n  }\n\n  readTimingPoints(): TimingPoint[] {\n    const count = this.readInt();\n    const list: TimingPoint[] = [];\n    for (let i = 0; i < count; i++) {\n      const bpm = this.readDouble();\n      const offset = this.readDouble();\n      const inherited = this.readBoolean();\n      list.push({ bpm, offset, inherited });\n    }\n    return list;\n  }\n\n  readBeatmap(version: number): Beatmap {\n    // TODO:\n    const bytesOfBeatmapEntry = version <= 20191107 ? this.readInt() : 0;\n\n    const artist: string = this.readString();\n    const artistUnicode: string = this.readString();\n    const title: string = this.readString();\n    const titleUnicode: string = this.readString();\n    const creator: string = this.readString();\n    const difficulty: string = this.readString();\n    const audioFileName: string = this.readString();\n    const md5Hash: string = this.readString();\n    const fileName: string = this.readString();\n    const rankedStatus: number = this.readByte();\n    const circlesCount: number = this.readShort();\n    const slidersCount: number = this.readShort();\n    const spinnersCount: number = this.readShort();\n    const lastModifiedTime: bigint = this.readLong();\n\n    // This is relevant since we will read either 1 byte or 4 bytes.\n    const difficultyReader = () => (version <= 20140609 ? this.readByte() : this.readSingle());\n    const approachRate: number = difficultyReader();\n    const circleSize: number = difficultyReader();\n    const hpDrain: number = difficultyReader();\n    const overallDifficulty: number = difficultyReader();\n\n    const sliderVelocity: number = this.readDouble();\n    const starRatings = () => (version >= 20140609 ? this.readStarRatings() : []);\n    const stdStarRatings: StarRatings = starRatings();\n    const taikoStarRatings: StarRatings = starRatings();\n    const catchStarRatings: StarRatings = starRatings();\n    const maniaStarRatings: StarRatings = starRatings();\n    const drainTime: number = this.readInt();\n    const totalTime: number = this.readInt();\n    const audioPreviewTime: number = this.readInt();\n    const timingPoints: TimingPoint[] = this.readTimingPoints();\n    const beatmapId: number = this.readInt();\n    const beatmapSetId: number = this.readInt();\n    const threadId: number = this.readInt();\n    const stdGrade: number = this.readByte();\n    const taikoGrade: number = this.readByte();\n    const ctbGrade: number = this.readByte();\n    const maniaGrade: number = this.readByte();\n    const localOffset: number = this.readShort();\n    const stackLeniency: number = this.readSingle();\n    const gameplayMode: number = this.readByte();\n    const source = this.readString();\n    const tags = this.readString();\n    const offset: number = this.readShort();\n    const titleFont = this.readString();\n    const isUnplayed: boolean = this.readBoolean();\n    const lastPlayed: bigint = this.readDateTime(); // readDateTime() or readLong()? on wiki it says Long\n    const isOsz2: boolean = this.readBoolean();\n    const folderName = this.readString();\n    const lastCheckedAgainstOsuRepo = this.readDateTime();\n    const ignoreBeatmapSound: boolean = this.readBoolean();\n    const ignoreBeatmapSkin: boolean = this.readBoolean();\n    const disableStoryboard: boolean = this.readBoolean();\n    const disableVideo: boolean = this.readBoolean();\n    const visualOverride: boolean = this.readBoolean();\n    if (version <= 20140609) this.readShort();\n    const lastModificationTime = this.readInt(); // ? There is already a last modified time above\n    const maniaScrollSpeed = this.readByte();\n    return {\n      bytesOfBeatmapEntry,\n      artist,\n      artistUnicode,\n      title,\n      titleUnicode,\n      creator,\n      difficulty,\n      audioFileName,\n      md5Hash,\n      fileName,\n      rankedStatus,\n      circlesCount,\n      slidersCount,\n      spinnersCount,\n      lastModifiedTime,\n      approachRate,\n      circleSize,\n      hpDrain,\n      overallDifficulty,\n      sliderVelocity,\n      stdStarRatings,\n      taikoStarRatings,\n      catchStarRatings,\n      maniaStarRatings,\n      drainTime,\n      totalTime,\n      audioPreviewTime,\n      timingPoints,\n      beatmapId,\n      beatmapSetId,\n      threadId,\n      stdGrade,\n      taikoGrade,\n      ctbGrade,\n      maniaGrade,\n      localOffset,\n      stackLeniency,\n      gameplayMode,\n      source,\n      tags,\n      offset,\n      titleFont,\n      isUnplayed,\n      lastPlayed,\n      isOsz2,\n      folderName,\n      lastCheckedAgainstOsuRepo,\n      ignoreBeatmapSound,\n      ignoreBeatmapSkin,\n      disableStoryboard,\n      disableVideo,\n      visualOverride,\n      maniaScrollSpeed,\n    };\n  }\n\n  readBeatmaps = (count: number, version: number) => {\n    const beatmaps: Beatmap[] = [];\n    // count = 1;\n    for (let i = 0; i < count; i++) {\n      beatmaps.push(this.readBeatmap(version) as Beatmap);\n    }\n    return beatmaps;\n  };\n\n  readOsuDB = async (): Promise<OsuDB> => {\n    const osuVersion = this.readInt();\n    const folderCount = this.readInt();\n    const accountIsUnlocked = this.readBoolean();\n    const accountUnlockDate = this.readDateTime();\n    const playerName = this.readString();\n    const numberOfBeatmaps = this.readInt();\n    const beatmaps = this.readBeatmaps(numberOfBeatmaps, osuVersion);\n    const userPermissions = this.readInt();\n\n    return {\n      osuVersion,\n      folderCount,\n      accountIsUnlocked,\n      accountUnlockDate,\n      playerName,\n      numberOfBeatmaps,\n      beatmaps,\n      userPermissions,\n    };\n  };\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/src/ScoresDBReader.ts",
    "content": "import { Reader } from \"./DatabaseReader\";\nimport { Score, ScoresDB } from \"./DatabaseTypes\";\n\nexport class ScoresDBReader extends Reader {\n  private readScore(): Score {\n    const gameplayMode = this.readByte();\n    const version = this.readInt();\n    const beatmapMD5 = this.readString();\n    const player = this.readString();\n    const replayMD5 = this.readString();\n    const numberOf300s = this.readShort();\n    const numberOf100s = this.readShort();\n    const numberOf50s = this.readShort();\n    const numberOfGekis = this.readShort();\n    const numberOfKatus = this.readShort();\n    const numberOfMisses = this.readShort();\n    const replayScore = this.readInt();\n    const maxCombo = this.readShort();\n    const perfectCombo = this.readBoolean();\n    const modsBitmask = this.readInt();\n    const emptyString = this.readString(); // should always be empty\n    const timestamp = this.readLong(); // 64Bit number\n    const minus1 = this.readInt(); // should always be 0xffffffff (-1)\n    const onlineScoreId = this.readLong();\n    // 23 is the bit of target practice\n    const targetPracticeEnabled = ((modsBitmask >> 23) & 1) > 0;\n    const additionalModInfo = targetPracticeEnabled ? this.readDouble() : 0;\n\n    return {\n      gameplayMode,\n      version,\n      beatmapMD5,\n      player,\n      replayMD5,\n      numberOf50s,\n      numberOf100s,\n      numberOf300s,\n      numberOfGekis,\n      numberOfKatus,\n      numberOfMisses,\n      replayScore,\n      maxCombo,\n      perfectCombo,\n      modsBitmask,\n      timestamp,\n      onlineScoreId,\n      additionalModInfo,\n    };\n  }\n\n  private readScores(numberOfScores: number): Score[] {\n    const scores: Score[] = [];\n    for (let i = 0; i < numberOfScores; i++) {\n      scores.push(this.readScore());\n    }\n    return scores;\n  }\n\n  private readBeatmap() {\n    const md5hash = this.readString();\n    const numberOfScores = this.readInt();\n    const scores = this.readScores(numberOfScores);\n    return {\n      md5hash,\n      numberOfScores,\n      scores,\n    };\n  }\n\n  private readBeatmaps(numberOfBeatmaps: number) {\n    const beatmaps: any[] = [];\n    for (let i = 0; i < numberOfBeatmaps; i++) {\n      beatmaps.push(this.readBeatmap());\n    }\n    return beatmaps;\n  }\n\n  readScoresDB(): ScoresDB {\n    const version = this.readInt();\n    const numberOfBeatmaps = this.readInt();\n    const beatmaps = this.readBeatmaps(numberOfBeatmaps);\n\n    return {\n      version,\n      numberOfBeatmaps,\n      beatmaps,\n    };\n  }\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/src/index.ts",
    "content": "export { OsuDBReader } from \"./OsuDBReader\";\nexport { ScoresDBReader } from \"./ScoresDBReader\";\nexport * from \"./DatabaseTypes\";\n"
  },
  {
    "path": "libs/osu-local/db-reader/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": [\n      \"node\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/db-reader/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/gosumemory/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu-local/gosumemory/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/gosumemory/README.md",
    "content": "# osu-local-gosumemory\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test osu-local-gosumemory` to execute the unit tests via [Jest](https://jestjs.io).\n"
  },
  {
    "path": "libs/osu-local/gosumemory/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-local-gosumemory\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu-local/gosumemory\",\n};\n"
  },
  {
    "path": "libs/osu-local/gosumemory/src/gosumemory.ts",
    "content": "// Version 1.3.3\n\nexport type GosuMemoryAPI = {\n  settings: {\n    showInterface: boolean;\n    folders: { game: string; skin: string; songs: string };\n  };\n  menu: {\n    mainMenu: { bassDensity: number };\n    state: OsuMemoryStatus;\n    gameMode: number;\n    isChatEnabled: number;\n    bm: {\n      time: { firstObj: number; current: number; full: number; mp3: number };\n      id: number;\n      set: number;\n      md5: string;\n      rankedStatus: number;\n      metadata: { artist: string; title: string; mapper: string; difficulty: string };\n      stats: {\n        AR: number;\n        CS: number;\n        OD: number;\n        HP: number;\n        SR: number;\n        BPM: { min: number; max: number };\n        maxCombo: number;\n        fullSR: number;\n        memoryAR: number;\n        memoryCS: number;\n        memoryOD: number;\n        memoryHP: number;\n      };\n      path: { full: string; folder: string; file: string; bg: string; audio: string };\n    };\n    mods: { num: number; str: string };\n    pp: { \"100\": number; \"99\": number; \"98\": number; \"97\": number; \"96\": number; \"95\": number; strains: number[] };\n  };\n  gameplay: {\n    gameMode: number;\n    name: string;\n    score: number;\n    accuracy: number;\n    combo: { current: number; max: number };\n    hp: { normal: number; smooth: number };\n    hits: {\n      \"300\": number;\n      geki: number;\n      \"100\": number;\n      katu: number;\n      \"50\": number;\n      \"0\": number;\n      sliderBreaks: number;\n      grade: { current: string; maxThisPlay: string };\n      unstableRate: number;\n      hitErrorArray: number[] | null;\n    };\n    pp: { current: number; fc: number; maxThisPlay: number };\n    keyOverlay: {\n      k1: { isPressed: boolean; count: number };\n      k2: { isPressed: boolean; count: number };\n      m1: { isPressed: boolean; count: number };\n      m2: { isPressed: boolean; count: number };\n    };\n    leaderboard: {\n      hasLeaderboard: boolean;\n      isVisible: boolean;\n      ourplayer: {\n        name: string;\n        score: number;\n        combo: number;\n        maxCombo: number;\n        mods: string;\n        h300: number;\n        h100: number;\n        h50: number;\n        h0: number;\n        team: number;\n        position: number;\n        isPassing: number;\n      };\n      slots: null;\n    };\n  };\n  resultsScreen: {\n    name: string;\n    score: number;\n    maxCombo: number;\n    mods: { num: number; str: string };\n    \"300\": number;\n    geki: number;\n    \"100\": number;\n    katu: number;\n    \"50\": number;\n    \"0\": number;\n  };\n  tourney: {\n    manager: {\n      ipcState: number;\n      bestOF: number;\n      teamName: { left: string; right: string };\n      stars: { left: number; right: number };\n      bools: { scoreVisible: boolean; starsVisible: boolean };\n      chat: string | null;\n      gameplay: { score: { left: number; right: number } };\n    };\n    ipcClients: null;\n  };\n};\n\nexport enum OsuMemoryStatus {\n  NotRunning = -1,\n  MainMenu = 0,\n  EditingMap = 1,\n  Playing = 2,\n  GameShutdownAnimation = 3,\n  SongSelectEdit = 4,\n  SongSelect = 5,\n  WIP_NoIdeaWhatThisIs = 6,\n  ResultsScreen = 7,\n  GameStartupAnimation = 10,\n  MultiplayerRooms = 11,\n  MultiplayerRoom = 12,\n  MultiplayerSongSelect = 13,\n  MultiplayerResultsscreen = 14,\n  OsuDirect = 15,\n  RankingTagCoop = 17,\n  RankingTeam = 18,\n  ProcessingBeatmaps = 19,\n  Tourney = 22,\n\n  /// <summary>\n  /// Indicates that status read in osu memory is not defined in <see cref=\"OsuMemoryStatus\"/>\n  /// </summary>\n  Unknown = -2,\n}\n"
  },
  {
    "path": "libs/osu-local/gosumemory/src/index.ts",
    "content": "export * from \"./gosumemory\";\n"
  },
  {
    "path": "libs/osu-local/gosumemory/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/gosumemory/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": []\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/gosumemory/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/osr-reader/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu-local/osr-reader/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/osr-reader/README.md",
    "content": "# osu-local-osr-reader\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test osu-local-osr-reader` to execute the unit tests via [Jest](https://jestjs.io).\n"
  },
  {
    "path": "libs/osu-local/osr-reader/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-local-osr-reader\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu-local/osr-reader\",\n};\n"
  },
  {
    "path": "libs/osu-local/osr-reader/src/index.ts",
    "content": "// TODO: I want a custom implementation\nexport { readSync, read, Replay as OsrReplay } from \"node-osr\";\n"
  },
  {
    "path": "libs/osu-local/osr-reader/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/osr-reader/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": [\n      \"node\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/osr-reader/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/skin-reader/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu-local/skin-reader/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/skin-reader/README.md",
    "content": "# osu-local-skin-reader\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test osu-local-skin-reader` to execute the unit tests via [Jest](https://jestjs.io).\n"
  },
  {
    "path": "libs/osu-local/skin-reader/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-local-skin-reader\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu-local/skin-reader\",\n};\n"
  },
  {
    "path": "libs/osu-local/skin-reader/src/SkinFolderReader.ts",
    "content": "import { posix } from \"path\";\nimport { promises as fs } from \"fs\";\nimport { generateDefaultSkinConfig, parseSkinIni } from \"@rewind/osu/skin\";\nimport { OsuLegacySkinTextureResolver } from \"./SkinTextureResolver\";\n\nconst join = posix.join;\n\nconst SKIN_CONFIG_FILENAME = \"skin.ini\";\n\ninterface ListSkinsInFolderOptions {\n  skinIniRequired: boolean;\n}\n\nconst defaultListingOptions: ListSkinsInFolderOptions = {\n  skinIniRequired: true,\n};\n\n/**\n * Provides functions to read local skins.\n */\nexport class SkinFolderReader {\n  /**\n   * Lists the directory names that should resemble the skin folders in the given Skins folder.\n   * @param skinsFolderPath  the path to check the skins\n   * @param options\n   */\n  static async listSkinsInFolder(\n    skinsFolderPath: string,\n    options?: Partial<ListSkinsInFolderOptions>,\n  ): Promise<string[]> {\n    const { skinIniRequired } = { ...defaultListingOptions, ...options };\n\n    const files = await fs.readdir(skinsFolderPath, { withFileTypes: true });\n    // This could be optimized by doing async calls in parallel\n    const results = await Promise.all(\n      files.map(async (f) => {\n        if (!f.isDirectory()) return null;\n\n        // Instant return if we don't need to check for a skin.ini file\n        if (!skinIniRequired) return f.name;\n\n        const skinConfigFile = join(skinsFolderPath, f.name, SKIN_CONFIG_FILENAME);\n        // This is recommended over .exists() which is deprecated\n        try {\n          const file = await fs.stat(skinConfigFile);\n          return file.isFile() ? f.name : null;\n        } catch (err) {\n          return null;\n        }\n      }),\n    );\n    return results.filter((f) => f !== null) as string[];\n  }\n\n  /**\n   * Reads the config file in the given skin folder and prepares a skin reader based on the given config.\n   * In case no config files exists, a default one will be generated according to\n   * https://osu.ppy.sh/wiki/el/Skinning/skin.ini\n   * @param skinFolderPath path to the folder\n   */\n  static async getSkinResolver(skinFolderPath: string): Promise<OsuLegacySkinTextureResolver> {\n    let config;\n    try {\n      const data = await fs.readFile(join(skinFolderPath, SKIN_CONFIG_FILENAME), { encoding: \"utf-8\" });\n      config = parseSkinIni(data);\n    } catch (err) {\n      config = generateDefaultSkinConfig(false);\n    }\n    return new OsuLegacySkinTextureResolver(skinFolderPath, config);\n  }\n}\n"
  },
  {
    "path": "libs/osu-local/skin-reader/src/SkinTextureResolver.ts",
    "content": "import {\n  comboDigitFonts,\n  DEFAULT_SKIN_TEXTURE_CONFIG,\n  defaultDigitFonts,\n  hitCircleDigitFonts,\n  OsuSkinTextures,\n  SkinConfig,\n} from \"@rewind/osu/skin\";\nimport { promises as fs } from \"fs\";\nimport * as Path from \"path\";\n\nexport type GetTextureFileOption = {\n  hdIfExists?: boolean;\n  animatedIfExists?: boolean;\n  tryWithFallback?: boolean;\n};\n\n// If F is fallback skin for main skin S\n// First check S/circle@2x.png then S/circle.png then F/circle@2x.png then F/circle.png.\nexport type OsuFileNameOptions = {\n  useHD: boolean;\n  extension: string;\n  animationIndex?: number;\n};\n\nexport type TextureFileLocation = {\n  key: string;\n  paths: string[];\n};\n\nexport interface OsuSkinTextureResolver {\n  /**\n   * Resolves the paths to the given skin texture. In case the given skin texture is animated, multiple paths\n   * will be returned.\n   *\n   * @param osuSkinTexture\n   * @param option\n   */\n  resolve(osuSkinTexture: OsuSkinTextures, option: GetTextureFileOption): Promise<string[]>;\n}\n\nconst join = Path.posix.join;\n\nexport class OsuLegacySkinTextureResolver implements OsuSkinTextureResolver {\n  constructor(readonly folderPath: string, readonly config: SkinConfig) {}\n\n  // Special case for font textures since their `prefix` can be changed.\n  // Best example would be WhiteCat 1.0 Skin where for example the combo prefix is `Assets\\combo` -> even different\n  // folder\n  getFontPrefix(skinTexture: OsuSkinTextures): string | undefined {\n    const { hitCirclePrefix, comboPrefix, scorePrefix } = this.config.fonts;\n\n    // join will also convert / to \\\\ if it's in Windows.\n    // This will be then Assets/combo/default0 for example\n    const combine = (prefix: string, id: number) => `${prefix}-${id}`;\n    {\n      const hitCircleId = hitCircleDigitFonts.indexOf(skinTexture as any);\n      if (hitCircleId !== -1) {\n        return combine(hitCirclePrefix, hitCircleId);\n      }\n    }\n    {\n      const comboId = comboDigitFonts.indexOf(skinTexture as any);\n      if (comboId !== -1) {\n        return combine(comboPrefix, comboId);\n      }\n    }\n    {\n      const scoreId = defaultDigitFonts.indexOf(skinTexture as any);\n      if (scoreId !== -1) {\n        return combine(scorePrefix, scoreId);\n      }\n    }\n\n    // Special cases\n    const texConfig = DEFAULT_SKIN_TEXTURE_CONFIG[skinTexture];\n    switch (skinTexture) {\n      case \"SCORE_PERCENT\":\n      case \"SCORE_DOT\":\n        return texConfig.filePrefix.replace(\"score\", scorePrefix);\n      case \"SCORE_X\":\n        return texConfig.filePrefix.replace(\"score\", comboPrefix);\n    }\n\n    return undefined;\n  }\n\n  getPrefix(skinTexture: OsuSkinTextures) {\n    const fontPrefix = this.getFontPrefix(skinTexture);\n    if (fontPrefix !== undefined) return fontPrefix;\n\n    const texConfig = DEFAULT_SKIN_TEXTURE_CONFIG[skinTexture];\n    if (!texConfig) {\n      throw Error(\"what u doing\");\n    }\n    // Maybe there is something with prefix in config in between (for numbers for example)\n    // FilePrefix could be overridden by skin config\n    // TODO: ??? join ???\n    return join(texConfig.filePrefix);\n  }\n\n  // No fallback, just straight up returns the filename\n  getFilename(skinTexture: OsuSkinTextures, opt: OsuFileNameOptions) {\n    const { useHD, animationIndex, extension } = opt;\n    const texConfig = DEFAULT_SKIN_TEXTURE_CONFIG[skinTexture];\n    if (!texConfig) {\n      throw Error(\"Config for given texture not found\");\n    }\n    const { skipHyphen, animationFrameRate } = texConfig;\n    let s = this.getPrefix(skinTexture);\n\n    // Notice animationIndex can also be 0, so checking if(animationIndex) won't work.\n    if (animationIndex !== undefined) {\n      // small sanity check against the config\n      if (animationFrameRate === undefined) throw Error(\"Don't use animationIndex for a non animatable texture\");\n      if (!skipHyphen) s += \"-\"; // There are some textures that don't use hyphen for animations such as sliderb0.png\n      s += animationIndex;\n    }\n    if (useHD) {\n      s += \"@2x\";\n    }\n    s += \".\" + extension;\n    return s;\n  }\n\n  async checkForFileExistenceInFolder(fileName: string) {\n    try {\n      const file = await fs.stat(join(this.folderPath, fileName));\n      // Technically we can also do more checks like: is it an image etc.\n      return file.isFile();\n    } catch (err) {\n      return false;\n    }\n  }\n\n  async checkForFirstAppearanceInFolder(fileNames: string[]) {\n    // Goes one by one through the list and returns the first one that exists.\n    for (const f of fileNames) {\n      const exists = await this.checkForFileExistenceInFolder(f);\n      if (exists) {\n        return f;\n      }\n    }\n    return null;\n  }\n\n  async checkForTextureWithFallback(skinTexture: OsuSkinTextures, options: OsuFileNameOptions) {\n    const filesToCheck = [this.getFilename(skinTexture, options)];\n    if (options.useHD) filesToCheck.push(this.getFilename(skinTexture, { ...options, useHD: false }));\n    return this.checkForFirstAppearanceInFolder(filesToCheck);\n  }\n\n  // We are not going to return the absolute path, just the relative path in the skin folder.\n  async resolve(skinTexture: OsuSkinTextures, option: GetTextureFileOption = {}): Promise<string[]> {\n    const { animatedIfExists, hdIfExists } = option;\n    const texConfig = DEFAULT_SKIN_TEXTURE_CONFIG[skinTexture];\n    if (!texConfig) {\n      throw Error(`Skin texture ${skinTexture} not found in config`);\n    }\n    // TODO: I think only menu-background.jpg uses jpg.\n    const extension = \"png\";\n\n    const tryWithAnimation = animatedIfExists && texConfig.animationFrameRate !== undefined;\n    if (tryWithAnimation) {\n      // Check for 0, 1, 2, ...\n      let animationIndex = 0;\n      const files: string[] = [];\n      // Can't be more than that right?\n      while (animationIndex < 727) {\n        const file = await this.checkForTextureWithFallback(skinTexture, {\n          useHD: hdIfExists ?? false,\n          extension: \"png\",\n          animationIndex,\n        });\n        if (file === null) {\n          break;\n        }\n        animationIndex += 1;\n        files.push(file);\n      }\n      if (files.length > 0) return files;\n      // Otherwise we fall back to non animated...\n    }\n    // Try with non-animated files\n    const defaultFile = await this.checkForTextureWithFallback(skinTexture, {\n      useHD: hdIfExists ?? false,\n      extension,\n    });\n    if (defaultFile) {\n      return [defaultFile];\n    }\n    // In the special case where we request no animation but there are only animated files (e.g. hit300k-0.png in\n    // WhiteCat's skin)\n    if (texConfig.animationFrameRate !== undefined) {\n      const fallbackFirstAnimationFile = await this.checkForTextureWithFallback(skinTexture, {\n        useHD: hdIfExists ?? false,\n        extension,\n        animationIndex: 0,\n      });\n      if (fallbackFirstAnimationFile) {\n        return [fallbackFirstAnimationFile];\n      }\n    }\n\n    // If here we find nothing, we don't ask for fallbackSkin\n    return [];\n  }\n}\n"
  },
  {
    "path": "libs/osu-local/skin-reader/src/index.ts",
    "content": "export * from \"./SkinFolderReader\";\nexport * from \"./SkinTextureResolver\";\n"
  },
  {
    "path": "libs/osu-local/skin-reader/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/skin-reader/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": [\n      \"node\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/skin-reader/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/utils/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu-local/utils/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/utils/README.md",
    "content": "# osu-local-utils\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test osu-local-utils` to execute the unit tests via [Jest](https://jestjs.io).\n"
  },
  {
    "path": "libs/osu-local/utils/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-local-utils\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu-local/utils\",\n};\n"
  },
  {
    "path": "libs/osu-local/utils/src/dates.ts",
    "content": "// Numbers taken from https://tickstodatetime.azurewebsites.net/\nconst epochTicks = BigInt(621355968000000000);\nconst ticksPerMillisecond = BigInt(10000);\nconst maxDateMilliseconds = BigInt(8640000000000000); // via\n// http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1\n\n// The last number is a number less than 10000\ntype WindowsDate = [Date, number];\n\nexport const ticksToDate = (ticks: bigint): WindowsDate => {\n  // convert the ticks into something javascript understands\n  const ticksSinceEpoch = ticks - epochTicks;\n  const millisecondsSinceEpoch = ticksSinceEpoch / ticksPerMillisecond;\n  const subMs = Number(ticksSinceEpoch % ticksPerMillisecond);\n\n  if (millisecondsSinceEpoch > maxDateMilliseconds) {\n    throw Error(`Given ticks=${ticks} would be too large`);\n  }\n  return [new Date(Number(millisecondsSinceEpoch)), subMs];\n};\n\nexport const dateToTicks = (wDate: WindowsDate): bigint => {\n  // convert the ticks into something javascript understands\n  const [date, subMs] = wDate;\n  const ms = BigInt(date.getTime()) * ticksPerMillisecond;\n  return ms + epochTicks + BigInt(subMs);\n};\n"
  },
  {
    "path": "libs/osu-local/utils/src/files.ts",
    "content": "import { constants, promises } from \"fs\";\nimport { access, mkdir, readFile } from \"fs/promises\";\nimport { join, resolve } from \"path\";\nimport { osuUserConfigParse } from \"./osuUserConfig\";\n\nconst { readdir, stat } = promises;\n\nexport const fileLastModifiedTime = async (path: string): Promise<number> => {\n  const s = await stat(path);\n  // mtime is changed when there was a write to the content\n  // ctime is for meta data changes like filename/access change\n  return s.mtimeMs;\n};\n\nexport const fileLastModifiedDate = async (path: string): Promise<Date> => {\n  const s = await stat(path);\n  return s.mtime;\n};\n\nconst filterUndefined = (p: any) => p !== undefined;\n\nexport async function filterFilenamesInDirectory(\n  dirName: string,\n  condition: (fileName: string) => Promise<boolean>,\n): Promise<string[]> {\n  const fileNamesInFolder = await readdir(dirName);\n  return (\n    await Promise.all(\n      fileNamesInFolder.map(async (fileName) => {\n        if (await condition(fileName)) {\n          return fileName;\n        } else {\n          return undefined;\n        }\n      }),\n    )\n  ).filter(filterUndefined) as string[];\n}\n\n// Just keeping it here just in case\nasync function createFolderIfNotExisting(folderPath: string) {\n  try {\n    await access(folderPath, constants.R_OK);\n  } catch (err) {\n    console.log(`Could not access the replays folder '${folderPath}'. Creating a new one`);\n    // TODO: Access rights?\n    return mkdir(folderPath);\n  }\n}\n\n// TODO: Need to test with really strange user names\nfunction osuUserConfigFileName(userName: string) {\n  return `osu!.${userName}.cfg`;\n}\n\n/**\n * Returns the absolute path to the songs folder.\n * @param osuFolderPath\n * @param userName\n */\nexport async function determineSongsFolder(osuFolderPath: string, userName: string) {\n  // First read the username specific config file that has important information such as the\n  const userConfigPath = join(osuFolderPath, osuUserConfigFileName(userName));\n\n  try {\n    const data = await readFile(userConfigPath, \"utf-8\");\n    const records = osuUserConfigParse(data);\n    const beatmapDirectory = records[\"BeatmapDirectory\"];\n    return resolve(osuFolderPath, beatmapDirectory);\n  } catch (err) {\n    return join(osuFolderPath, \"Songs\");\n  }\n}\n"
  },
  {
    "path": "libs/osu-local/utils/src/index.ts",
    "content": "export * from \"./dates\";\nexport * from \"./files\";\nexport * from \"./stable\";\nexport * from \"./osuUserConfig\";\n"
  },
  {
    "path": "libs/osu-local/utils/src/osuUserConfig.spec.ts",
    "content": "import { osuUserConfigParse } from \"./osuUserConfig\";\n\ntest(\"Parsing osu!.[username].cfg config\", async () => {\n  const data = `\n# osu! configuration for me\n# last updated on Tuesday, November 2, 2021\n\n# IMPORTANT: DO NOT SHARE THIS FILE PUBLICLY.\n# IT CONTAINS YOUR LOGIN CREDENTIALS IF YOU HAVE THEM SAVED.\n\nBeatmapDirectory = Songs\nVolumeUniversal = 65\nVolumeEffect = 100\nVolumeMusic = 30\nSkin = - Amaestric [1.1]\nSkinWithSpaceAtTheEnd = WhiteCat 1.0\\u0020\n\n# Comment = Should Be Ignored\n`;\n  // \\u0020 is the explicit space character\n\n  // I also tested BeatmapDirectory = \"SomethingInQuotations\" and this won't be parsed correctly by osu!\n\n  const records = osuUserConfigParse(data);\n\n  expect(records[\"BeatmapDirectory\"]).toEqual(\"Songs\");\n  expect(records[\"VolumeMusic\"]).toEqual(\"30\");\n  expect(records[\"Skin\"]).toEqual(\"- Amaestric [1.1]\");\n  expect(records[\"SkinWithSpaceAtTheEnd\"]).toEqual(\"WhiteCat 1.0 \");\n  expect(records[\"Comment\"]).toBeUndefined();\n});\n"
  },
  {
    "path": "libs/osu-local/utils/src/osuUserConfig.ts",
    "content": "export function osuUserConfigParse(data: string) {\n  const lines = data.split(/\\r?\\n/);\n  const records: Record<string, string> = {};\n\n  for (const line of lines) {\n    if (line.startsWith(\"#\")) continue;\n    // After osu! closes it will always flush the config file with a valid format (spaces included)\n    const [key, value] = line.split(\" = \");\n    if (value !== undefined) {\n      records[key] = value;\n    }\n  }\n  return records;\n}\n"
  },
  {
    "path": "libs/osu-local/utils/src/stable.ts",
    "content": "// This correspond to exact 1600 years\n// 1601-01-01 00:00:00.000 basically\nconst magicConstant = BigInt(\"504911232000000000\");\nexport const legacyReplayFileName = (beatmapMD5Hash: string, scoreTimeStamp: bigint): string => {\n  const magicTicks = scoreTimeStamp - magicConstant;\n  return `${beatmapMD5Hash}-${magicTicks}.osr`;\n};\n"
  },
  {
    "path": "libs/osu-local/utils/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/utils/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": [\n      \"node\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-local/utils/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/README.md",
    "content": "# osu! classic components\n\nThese UI elements are designed to match osu!stable as close as possible.\n\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-pixi-classic-components\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu-pixi/classic-components\",\n};\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/DrawableSettings.ts",
    "content": "import { Position } from \"@osujs/math\";\n\nexport interface AnimationTimeSetting {\n  time: number;\n}\n\nexport interface ModHiddenSetting {\n  modHidden: boolean;\n}\nexport interface PositionSetting {\n  position: Position;\n}\n\nexport interface ScaleSetting {\n  scale: number;\n}\n\nexport interface TintSetting {\n  tint: number;\n}\n\n// Replaces ArmedState\n//  -> if there is no HitResult => IDLE\n//  -> if there is one then we also know the timing\nexport type HitResult = {\n  hit: boolean;\n  timing: number;\n};\nexport type HitResultSetting = {\n  hitResult?: HitResult;\n};\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicApproachCircle.ts",
    "content": "import { Sprite } from \"@pixi/sprite\";\nimport { AnimationTimeSetting, ModHiddenSetting } from \"../DrawableSettings\";\nimport { PrepareSetting } from \"../utils/Preparable\";\nimport { Position } from \"@osujs/math\";\nimport { Texture } from \"pixi.js\";\nimport {\n  applyPropertiesToDisplayObject,\n  createCenteredSprite,\n  DisplayObjectTransformationProcess,\n  evaluateTransformationsToProperties,\n} from \"../utils/Pixi\";\nimport { OsuClassicConstants } from \"./OsuClassicConstants\";\nimport { fadeInT, fadeOutT, scaleToT } from \"../utils/Transformations\";\n\nexport interface OsuClassicApproachCircleSettings extends AnimationTimeSetting, ModHiddenSetting {\n  scale: number;\n  position: Position;\n  approachDuration: number;\n  texture: Texture;\n  tint: number;\n}\n\n// TODO: investigate (it was in McOsu)\nconst approachCircleMultiplier = 0.9;\n\nconst DEFAULT_SETTINGS: OsuClassicApproachCircleSettings = {\n  time: 0,\n  modHidden: false,\n  approachDuration: 450, // AR10\n  scale: 0.57, // CS4\n  position: { x: 0, y: 0 },\n\n  texture: Texture.EMPTY,\n  tint: 0x111111,\n};\n\nexport class OsuClassicApproachCircle implements PrepareSetting<OsuClassicApproachCircleSettings> {\n  sprite: Sprite;\n  settings: OsuClassicApproachCircleSettings;\n\n  constructor(settings: Partial<OsuClassicApproachCircleSettings>) {\n    this.settings = { ...DEFAULT_SETTINGS, ...settings };\n    this.sprite = createCenteredSprite();\n  }\n\n  prepare(settings: Partial<OsuClassicApproachCircleSettings>): void {\n    this.settings = { ...this.settings, ...settings };\n    const { texture, tint } = this.settings;\n    this.sprite.texture = texture;\n    this.sprite.tint = tint;\n    this.animate();\n  }\n\n  normalTransformation(): DisplayObjectTransformationProcess {\n    const timeFadeIn = OsuClassicConstants.DEFAULT_FADE_IN_DURATION; // TODO: Check if correct\n    const hitTime = 0;\n    const { position, scale, approachDuration } = this.settings;\n    const spawnTime = hitTime - approachDuration;\n\n    const fadingDuration = Math.min(timeFadeIn * 2, approachDuration);\n\n    // TODO: if hitResult, need fadeOut\n\n    return {\n      position: {\n        startValue: position,\n      },\n      scale: {\n        startValue: scale * 4,\n        transformations: [{ time: [spawnTime, hitTime], func: scaleToT(scale * 1.0) }],\n      },\n      alpha: {\n        startValue: 0,\n        transformations: [\n          { time: [spawnTime, spawnTime + fadingDuration], func: fadeInT() },\n          { time: [hitTime, hitTime + 50], func: fadeOutT() },\n        ],\n      },\n    };\n  }\n\n  hiddenTransformation(): DisplayObjectTransformationProcess {\n    return {\n      alpha: {\n        startValue: 0,\n      },\n    };\n  }\n\n  animate(): void {\n    const { modHidden, time, scale, position } = this.settings;\n    const transformation = modHidden ? this.hiddenTransformation() : this.normalTransformation();\n    const props = evaluateTransformationsToProperties(transformation, time, { scale, position });\n    applyPropertiesToDisplayObject(props, this.sprite);\n  }\n\n  dispose(): void {\n    return;\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicConstants.ts",
    "content": "import { ModHiddenConstants } from \"@osujs/core\";\n\nexport const OsuClassicConstants = {\n  LEGACY_FADE_OUT_DURATION: 240,\n  DEFAULT_FADE_IN_DURATION: 400,\n  fadeInDurationMultiplier: 0.4,\n  fadeOutDurationMultiplier: ModHiddenConstants.FADE_OUT_DURATION_MULTIPLIER,\n};\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicCursor.ts",
    "content": "import { PrepareSetting } from \"../utils/Preparable\";\nimport { Texture } from \"pixi.js\";\nimport { Container } from \"@pixi/display\";\nimport { Sprite } from \"@pixi/sprite\";\nimport { createCenteredSprite } from \"../utils/Pixi\";\nimport { Position } from \"@osujs/math\";\n\nconst MAX_CURSOR_TRAILS = 8;\n\nexport interface OsuClassicCursorSetting {\n  // The position of the cursor\n  position: Position;\n\n  // 0th trail is the earliest and will have the highest alpha, the others will \"fade out\" just like in OsuClassic\n  // If you need to hide the cursor trail then just set this to `[]`.\n  trailPositions: Position[];\n\n  // If you need something like \"scale with CS\", then set this accordingly\n  cursorScale: number;\n\n  cursorTexture: Texture;\n  cursorTrailTexture: Texture;\n\n  // If motion blurring / similar gets implemented\n  // velocity: Vec2;\n\n  // Customizable fade out easing function?\n}\n\nconst defaultSettings: OsuClassicCursorSetting = {\n  position: { x: 0, y: 0 },\n  trailPositions: [],\n  cursorScale: 1.0,\n  cursorTexture: Texture.EMPTY,\n  cursorTrailTexture: Texture.EMPTY,\n  // velocity: Vec2.Zero,\n};\n\n/**\n * Cursor also has some animations -> for example when clicking it can expand or not depending on the setting.\n *\n * This implementation initializes a set of MAX_CURSOR cursor trail sprites.\n */\nexport class OsuClassicCursor implements PrepareSetting<OsuClassicCursorSetting> {\n  public container: Container;\n  private readonly cursorSprite: Sprite;\n  private readonly cursorTrailSprites: Sprite[];\n  private settings: OsuClassicCursorSetting; // So caching can be done later ...\n\n  constructor() {\n    this.container = new Container();\n    this.settings = defaultSettings;\n    this.cursorSprite = createCenteredSprite();\n    this.cursorTrailSprites = [];\n    for (let i = 0; i < MAX_CURSOR_TRAILS; i++) this.cursorTrailSprites.push(createCenteredSprite());\n\n    // We will add those sprites in reverse because the cursor should be ON TOP of the others.\n    for (let i = 0; i < MAX_CURSOR_TRAILS; i++) {\n      this.container.addChild(this.cursorTrailSprites[MAX_CURSOR_TRAILS - i - 1]);\n    }\n    this.container.addChild(this.cursorSprite);\n    this.container.position.set(0, 0);\n  }\n\n  prepare(setting: Partial<OsuClassicCursorSetting>): void {\n    this.settings = { ...this.settings, ...setting };\n\n    const { cursorScale, cursorTexture, cursorTrailTexture, trailPositions, position } = this.settings;\n\n    this.cursorSprite.texture = cursorTexture;\n\n    // The cursor is centered in the container at (0, 0) and the container is the one that gets \"moved\" around.\n    // The cursor trails have their positions relative to (0, 0) basically.\n    this.cursorSprite.position.set(position.x, position.y);\n    this.cursorSprite.scale.set(cursorScale);\n\n    // Does not work as expected\n    // this.cursorSprite.filters = [new MotionBlurFilter([90, 90], 25)];\n\n    // this.container.position.set(position.x, position.y);\n    this.cursorTrailSprites.forEach((cts, i) => {\n      cts.texture = cursorTrailTexture;\n      if (i < trailPositions.length) {\n        // const offset = Vec2.sub(trailPositions[i], position);\n        cts.position.set(trailPositions[i].x, trailPositions[i].y);\n        cts.alpha = (trailPositions.length - i) / trailPositions.length; // Linear (but might be configurable)\n        cts.scale.set(cursorScale);\n      } else {\n        cts.alpha = 0;\n      }\n    });\n    // this.container.scale.set(cursorScale);\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicHitCircleArea.ts",
    "content": "import { Container } from \"@pixi/display\";\nimport { Texture } from \"@pixi/core\";\nimport { Sprite } from \"@pixi/sprite\";\nimport { OsuClassicNumber } from \"../hud/OsuClassicNumber\";\nimport {\n  applyPropertiesToDisplayObject,\n  createCenteredSprite,\n  DisplayObjectTransformationProcess,\n  evaluateTransformationsToProperties,\n} from \"../utils/Pixi\";\nimport {\n  AnimationTimeSetting,\n  HitResult,\n  ModHiddenSetting,\n  PositionSetting,\n  ScaleSetting,\n  TintSetting,\n} from \"../DrawableSettings\";\nimport { fadeInT, fadeOutT, scaleToT } from \"../utils/Transformations\";\nimport { OsuClassicConstants } from \"./OsuClassicConstants\";\nimport { PrepareSetting } from \"../utils/Preparable\";\nimport { Easing } from \"@osujs/math\";\nimport { calculateDigits } from \"../utils/numbers\";\n\n// Legacy, in osu!lazer there is a bunch of more stuff such as triangles\n\nexport interface OsuClassicHitCircleAreaSettings\n  extends AnimationTimeSetting,\n    ModHiddenSetting,\n    TintSetting,\n    ScaleSetting,\n    PositionSetting {\n  // The number that is displayed inside the circle (comboIndex)\n  number: number;\n  numberOverlap: number;\n  numberScaling: number;\n\n  // The color tint of the hitCircleArea\n  tint: number;\n\n  // The textures to provide to the three sprites\n  hitCircleTexture: Texture;\n  numberTextures: Texture[]; // digit `i` has `numberTextures[i]`\n  hitCircleOverlayTexture: Texture;\n\n  // Usually the HitCircleArea is like a sandwich with the hitCircleTexture on the bottom and the overlay on the top\n  // with the number in between, except if hitCircleOverlayAboveNumber is false, then the number is on the top.\n  hitCircleOverlayAboveNumber: boolean;\n\n  // Animation relevant properties\n  // A not mutable property? Maybe as a constructor\n  approachDuration: number;\n  fadeInDuration: number;\n\n  scale: number;\n\n  hitResult: HitResult | null;\n}\n\n// Numbers in HitCircles are downscaled by 0.8, see: https://osu.ppy.sh/wiki/el/Skinning/osu%21\nconst DEFAULT_NUMBER_SCALING_IN_HIT_CIRCLE = 0.8;\nconst DEFAULT_NUMBER_TEXTURES: Texture[] = Array(10).fill(Texture.EMPTY);\n\nconst defaultSettings: OsuClassicHitCircleAreaSettings = {\n  // Very likely to change\n  time: 0,\n  // Likely to change\n  modHidden: false,\n  hitResult: null,\n\n  // Unlikely to change (rare events such as SkinChange, BeatmapDiff change)\n  // But this actually depends on the application, e.g., position could often change in an osu!editor.\n  number: 1,\n  numberOverlap: 0,\n  numberScaling: DEFAULT_NUMBER_SCALING_IN_HIT_CIRCLE,\n\n  hitCircleOverlayTexture: Texture.EMPTY,\n  numberTextures: DEFAULT_NUMBER_TEXTURES,\n  hitCircleTexture: Texture.EMPTY,\n\n  tint: 0x111111,\n  hitCircleOverlayAboveNumber: true,\n  approachDuration: 450, // AR10\n  scale: 0.57, // CS4\n  position: { x: 0, y: 0 },\n  fadeInDuration: OsuClassicConstants.DEFAULT_FADE_IN_DURATION, // 400ms\n};\n\n/**\n * Timing is relative to zero.\n * The animation starts at time `-approachDuration`.\n */\nexport class OsuClassicHitCircleArea implements PrepareSetting<OsuClassicHitCircleAreaSettings> {\n  public container: Container;\n  private hitCircleSprite: Sprite;\n  // TODO: Number also as sprite\n  private readonly number: OsuClassicNumber;\n  private hitCircleOverlaySprite: Sprite;\n\n  private settings: OsuClassicHitCircleAreaSettings; // 100% immutable\n\n  // Maybe use IOC for the children (since the sprites could come from a pool or something)\n  constructor(settings?: Partial<OsuClassicHitCircleAreaSettings>) {\n    this.container = new Container();\n    // This is the default order (in case no hitCircleOverlayAboveNumber)\n    this.container.addChild((this.hitCircleSprite = createCenteredSprite()));\n    this.container.addChild((this.number = new OsuClassicNumber()));\n    this.container.addChild((this.hitCircleOverlaySprite = createCenteredSprite()));\n    this.settings = Object.freeze({ ...defaultSettings, ...settings });\n  }\n\n  private prepareHitCircleSprites(): void {\n    const { hitCircleTexture, hitCircleOverlayTexture, tint } = this.settings;\n    this.hitCircleSprite.texture = hitCircleTexture;\n    this.hitCircleOverlaySprite.texture = hitCircleOverlayTexture;\n    this.hitCircleSprite.tint = tint;\n  }\n\n  private prepareNumber(): void {\n    const { number, numberTextures: textures, numberOverlap: overlap, scale } = this.settings;\n    const hideNumber = false; // TODO: Maybe as a setting?\n    if (hideNumber) this.number.renderable = false; // ??\n    this.number.prepare({ digits: calculateDigits(number), textures, overlap });\n    this.number.anchorX = 0.5;\n    this.number.anchorY = 0.5;\n  }\n\n  private prepareSpriteOrder(): void {\n    // Define the order by setting the zIndices\n    const { hitCircleOverlayAboveNumber } = this.settings;\n    this.hitCircleSprite.zIndex = 0;\n    if (hitCircleOverlayAboveNumber) {\n      this.number.zIndex = 1;\n      this.hitCircleOverlaySprite.zIndex = 2;\n    } else {\n      this.hitCircleOverlaySprite.zIndex = 1;\n      this.number.zIndex = 2;\n    }\n    this.container.sortChildren(); // Container.sortChildren() will do it by zIndex\n  }\n\n  private animateHitCircleSprites(): void {\n    const { time, hitResult } = this.settings;\n    const props = evaluateTransformationsToProperties(hitCircleSpritesTransform(hitResult), time, {\n      alpha: 1.0,\n      scale: 1.0,\n    });\n    [this.hitCircleSprite, this.hitCircleOverlaySprite].forEach((s) => applyPropertiesToDisplayObject(props, s));\n  }\n\n  private animateNumber(): void {\n    const { time, numberScaling, hitResult } = this.settings;\n    const props = evaluateTransformationsToProperties(numberTransform(hitResult), time, {\n      alpha: 1.0,\n      scale: numberScaling,\n    });\n    applyPropertiesToDisplayObject(props, this.number);\n  }\n\n  private animateSelf(): void {\n    const { time, hitResult, modHidden, approachDuration, fadeInDuration, scale, position } = this.settings;\n    const transforms = modHidden\n      ? hiddenHitCircleTransforms(approachDuration)\n      : normalHitCircleTransforms(approachDuration, fadeInDuration, hitResult);\n    const thisProps = evaluateTransformationsToProperties(transforms, time, { alpha: 1.0, scale, position });\n    applyPropertiesToDisplayObject(thisProps, this.container);\n  }\n\n  private animate(): void {\n    this.animateHitCircleSprites();\n    this.animateNumber();\n    this.animateSelf();\n  }\n\n  prepare(settings: Partial<OsuClassicHitCircleAreaSettings>): void {\n    this.settings = Object.freeze({ ...this.settings, ...settings });\n    this.prepareSpriteOrder();\n    this.prepareNumber();\n    this.prepareHitCircleSprites();\n    this.animate();\n  }\n}\n\n// Numbers fade away a bit faster than the HitCircleSprites\nfunction numberTransform(hitResult: HitResult | null): DisplayObjectTransformationProcess {\n  if (!hitResult || !hitResult.hit) {\n    return {};\n  }\n  const { timing } = hitResult;\n  return {\n    alpha: {\n      startValue: 1,\n      transformations: [\n        {\n          time: [timing, timing + OsuClassicConstants.LEGACY_FADE_OUT_DURATION / 4],\n          func: fadeOutT(0, Easing.OUT),\n        },\n      ],\n    },\n  };\n}\n\n// Fully visible at time 0\nfunction normalHitCircleTransforms(\n  approachDuration: number,\n  fadeInDuration: number,\n  hitResult: HitResult | null,\n): DisplayObjectTransformationProcess {\n  const delay = 800; // this is I think a small hack of lazer to wait for other sprites to transform\n  const supposedToHitTime = 0;\n  // TODO: also handle the case with hitResult undefined ...\n  const hit = hitResult && hitResult.hit;\n  return {\n    alpha: {\n      startValue: 0,\n      transformations: [\n        { time: [-approachDuration, -approachDuration + fadeInDuration], func: fadeInT() },\n        hit\n          ? { time: [supposedToHitTime + delay, supposedToHitTime + delay + 1], func: fadeOutT() }\n          : { time: [supposedToHitTime, supposedToHitTime + 100], func: fadeOutT() },\n      ],\n    },\n  };\n}\n\nfunction hiddenHitCircleTransforms(approachDuration: number): DisplayObjectTransformationProcess {\n  const fadeInDuration = approachDuration * OsuClassicConstants.fadeInDurationMultiplier;\n  const fadeOutDuration = approachDuration * OsuClassicConstants.fadeOutDurationMultiplier;\n  const fullyFadedInTime = -approachDuration + fadeInDuration;\n  return {\n    alpha: {\n      startValue: 0,\n      transformations: [\n        { time: [-approachDuration, -approachDuration + fadeInDuration], func: fadeInT() },\n        { time: [fullyFadedInTime, fullyFadedInTime + fadeOutDuration], func: fadeOutT() },\n      ],\n    },\n  };\n}\n\n// In case of HitResult at \"v\"\n//                      v\n// |--preemptDuration------|\n// |--fadeIn--|         |--fadeOut--|\n// If there is a miss, then .hit might also be relevant?\nfunction hitCircleSpritesTransform(hitResult: HitResult | null): DisplayObjectTransformationProcess {\n  if (!hitResult || !hitResult.hit) {\n    return {};\n  }\n  const { timing } = hitResult;\n  // Only if there is a hit we will make a small \"explosion\" animation\n  return {\n    alpha: {\n      startValue: 1,\n      transformations: [\n        {\n          time: [timing, timing + OsuClassicConstants.LEGACY_FADE_OUT_DURATION],\n          func: fadeOutT(0, Easing.OUT),\n        },\n      ],\n    },\n    scale: {\n      startValue: 1,\n      transformations: [\n        {\n          time: [timing, timing + OsuClassicConstants.LEGACY_FADE_OUT_DURATION],\n          func: scaleToT(1.4, Easing.OUT),\n        },\n      ],\n    },\n  };\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicJudgements.ts",
    "content": "// DefaultJudgementPiece.cs\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicSliderBall.ts",
    "content": "import { PrepareSetting } from \"../utils/Preparable\";\nimport { Container, Sprite, Texture } from \"pixi.js\";\nimport { createCenteredSprite } from \"../utils/Pixi\";\nimport { PositionSetting, ScaleSetting } from \"../DrawableSettings\";\n\n/**\n * Additional settings:\n * * timeSinceLostTrack\n * * timeSinceTracking\n * * ballRotation\n */\ninterface OsuClassicSliderBallSettings extends PositionSetting, ScaleSetting {\n  followCircleTexture: Texture;\n  ballTexture: Texture;\n  ballTint: number | null;\n}\n\nconst defaultSettings: OsuClassicSliderBallSettings = {\n  followCircleTexture: Texture.EMPTY,\n  ballTexture: Texture.EMPTY,\n  ballTint: null,\n  position: { x: 0, y: 0 },\n  scale: 0.57, // cs4\n};\n\n/**\n * trackingStart\n * notTrackingStart\n *\n * just so we can get animations\n *\n */\nexport class OsuClassicSliderBall implements PrepareSetting<OsuClassicSliderBallSettings> {\n  container: Container;\n  followCircleSprite: Sprite;\n  ballSprite: Sprite;\n\n  private settings: OsuClassicSliderBallSettings;\n\n  constructor() {\n    this.container = new Container();\n    this.followCircleSprite = createCenteredSprite();\n    this.ballSprite = createCenteredSprite();\n    this.settings = defaultSettings;\n\n    this.container.addChild(this.ballSprite);\n    this.container.addChild(this.followCircleSprite);\n  }\n\n  // TODO: Maybe move settings (and slider) to prepareFor ...\n  prepare(setting: Partial<OsuClassicSliderBallSettings>): void {\n    this.settings = { ...this.settings, ...setting };\n\n    const { followCircleTexture, ballTexture, position, ballTint, scale } = this.settings;\n\n    this.followCircleSprite.texture = followCircleTexture;\n    this.ballSprite.texture = ballTexture;\n\n    if (ballTint !== null) {\n      this.ballSprite.tint = ballTint;\n    }\n\n    // If the ball is tracking it is 2.4 and alpha = 1\n    // TODO: osu!magic^TM\n    this.followCircleSprite.scale.set(1.2);\n    this.container.position.set(position.x, position.y);\n    this.container.alpha = 1.0;\n    this.container.scale.set(scale);\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicSliderBody.ts",
    "content": "import { Sprite } from \"@pixi/sprite\";\nimport {\n  applyPropertiesToDisplayObject,\n  DisplayObjectTransformationProcess,\n  evaluateTransformationsToProperties,\n} from \"../utils/Pixi\";\nimport { Easing, Position } from \"@osujs/math\";\nimport { fadeInT, fadeOutT } from \"../utils/Transformations\";\nimport { PrepareSetting } from \"../utils/Preparable\";\nimport { Container, Texture } from \"pixi.js\";\nimport { AnimationTimeSetting, ModHiddenSetting } from \"../DrawableSettings\";\nimport { OsuClassicConstants } from \"./OsuClassicConstants\";\n\nexport interface SliderBodySettings extends AnimationTimeSetting, ModHiddenSetting {\n  approachDuration: number;\n  position: Position;\n  duration: number;\n  texture: Texture;\n  headPositionInRectangle: Position;\n}\n\nconst defaultSliderBodySetting: SliderBodySettings = {\n  time: 0,\n  approachDuration: 450, // AR10\n  position: { x: 0, y: 0 },\n  headPositionInRectangle: { x: 0, y: 0 },\n  modHidden: false,\n  duration: 0,\n  texture: Texture.EMPTY,\n};\n\n/**\n * Basically just a sprite that can animate according to the settings.\n *\n * The slider body texture has to be calculated.\n */\nexport class OsuClassicSliderBody implements PrepareSetting<SliderBodySettings> {\n  container: Container;\n  sprite: Sprite;\n  settings: SliderBodySettings;\n\n  constructor() {\n    this.settings = defaultSliderBodySetting;\n    this.container = new Container();\n    this.container.addChild((this.sprite = new Sprite()));\n  }\n\n  static transformation(settings: {\n    approachDuration: number;\n    position: Position;\n    modHidden: boolean;\n    duration: number;\n  }): DisplayObjectTransformationProcess {\n    const timeFadeIn = 400;\n    const { approachDuration, modHidden, position, duration } = settings;\n\n    const startTime = 0;\n    const endTime = duration;\n    const appearanceTime = startTime - approachDuration;\n\n    const defaultTransforms: DisplayObjectTransformationProcess = {\n      scale: { startValue: 1.0 },\n      position: { startValue: position },\n    };\n    if (modHidden) {\n      const hiddenFadeInDuration = approachDuration * OsuClassicConstants.fadeInDurationMultiplier;\n      return {\n        ...defaultTransforms,\n        alpha: {\n          startValue: 0.0,\n          transformations: [\n            { time: [appearanceTime, appearanceTime + hiddenFadeInDuration], func: fadeInT() },\n            { time: [appearanceTime + hiddenFadeInDuration, endTime], func: fadeOutT() },\n          ],\n        },\n      };\n    } else {\n      // Lazer specific\n      const fadeOutTime = 450;\n      return {\n        ...defaultTransforms,\n        alpha: {\n          startValue: 0.0,\n          transformations: [\n            { time: [appearanceTime, appearanceTime + timeFadeIn], func: fadeInT() },\n            { time: [endTime, endTime + fadeOutTime], func: fadeOutT(0, Easing.OUT_QUINT) },\n          ],\n        },\n      };\n    }\n  }\n\n  prepare(settings: Partial<SliderBodySettings>): void {\n    this.settings = Object.freeze({ ...this.settings, ...settings });\n\n    const { time, modHidden, position, approachDuration, duration, texture, headPositionInRectangle } = this.settings;\n\n    const t = OsuClassicSliderBody.transformation({ modHidden, duration, position, approachDuration });\n    const props = evaluateTransformationsToProperties(t, time);\n    applyPropertiesToDisplayObject(props, this.container);\n    this.sprite.texture = texture;\n    this.sprite.position.set(-headPositionInRectangle.x, -headPositionInRectangle.y);\n\n    // This is the most stupid caching\n    // if (!this.sprite.texture.valid)\n    //   this.sliderTextureManager.registerJob({\n    //     borderColor,\n    //     points,\n    //     radius,\n    //     sprite: this.sprite,\n    //   });\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicSliderRepeat.ts",
    "content": "import { Sprite, Texture } from \"pixi.js\";\nimport {\n  createCenteredSprite,\n  DisplayObjectTransformationProcess,\n  evaluateTransformationsToProperties,\n} from \"../utils/Pixi\";\nimport { AnimationTimeSetting, PositionSetting, ScaleSetting } from \"../DrawableSettings\";\nimport { PrepareSetting } from \"../utils/Preparable\";\nimport { fadeInT, fadeOutT, scaleToT, Transformation } from \"../utils/Transformations\";\nimport { Easing } from \"@osujs/math\";\n\ninterface OsuClassicSliderRepeatSettings extends PositionSetting, ScaleSetting, AnimationTimeSetting {\n  time: number;\n  texture: Texture;\n  rotationAngle: number;\n  // When it should start fading in and \"beating\"\n  approachDuration: number;\n  // In Lazer it is Math.min(300, SpanDuration), might also be known as `animDuration`\n  fadeInOutDuration: number;\n  beatLength: number;\n  hit: boolean | null;\n}\n\nconst defaultSettings: OsuClassicSliderRepeatSettings = {\n  time: 0,\n  scale: 0.57, // cs4\n  position: { x: 0, y: 0 },\n  texture: Texture.EMPTY,\n  rotationAngle: 0,\n  approachDuration: 450,\n  fadeInOutDuration: 300,\n  hit: null,\n  beatLength: 500, // 120bpm\n};\n\n/**\n * A bit simplified version of the osu!lazer one. No offset\n *\n * The actual animation has a really complicated logic, for more reading resources refer to those osu!lazer files:\n * * BeatSyncedContainer.cs\n * * ReverseArrowPiece.cs\n * * SliderEndCircle.cs\n * * DrawableSliderRepeat.cs\n */\nexport class OsuClassicSliderRepeat implements PrepareSetting<OsuClassicSliderRepeatSettings> {\n  sprite: Sprite;\n  settings: OsuClassicSliderRepeatSettings;\n\n  constructor() {\n    this.sprite = createCenteredSprite();\n    this.settings = defaultSettings;\n  }\n\n  static normalTransformation(settings: {\n    hit: boolean | null;\n    scale: number;\n    approachDuration: number;\n    fadeInOutDuration: number;\n    beatLength: number;\n  }): DisplayObjectTransformationProcess {\n    const { hit, scale, approachDuration, fadeInOutDuration, beatLength } = settings;\n\n    // So it's actually inaccurate for higher BPMs (>300) but shouldn't really be visible with the eye\n    const usedBeatLength = Math.min(beatLength, 200);\n    const scalingTransformations: Transformation<number>[] = [];\n    // hitTiming CAN ONLY BE at 0.\n    const hitTiming = 0;\n    // This might be slow for higher approach durations (such as -infinity AR)\n    let t = -approachDuration;\n    while (t < hitTiming) {\n      scalingTransformations.push({ time: [t, t + usedBeatLength - 1], func: scaleToT(1.0 * scale, Easing.OUT) });\n      // TODO That's not really clean to set the scale instantly to 1.3\n      scalingTransformations.push({ time: [t + usedBeatLength - 1, t], func: scaleToT(1.3 * scale, Easing.OUT) });\n      t += usedBeatLength;\n    }\n\n    const alphaTransformations: Transformation<number>[] = [\n      {\n        time: [-approachDuration, -approachDuration + fadeInOutDuration],\n        func: fadeInT(),\n      },\n    ];\n\n    if (hit !== null) {\n      if (hit) {\n        alphaTransformations.push({ time: [hitTiming, hitTiming + fadeInOutDuration], func: fadeOutT() });\n      } else {\n        alphaTransformations.push({\n          time: [hitTiming, hitTiming + fadeInOutDuration],\n          func: fadeOutT(0, Easing.OUT),\n        });\n      }\n    }\n\n    return {\n      alpha: {\n        startValue: 0,\n        transformations: alphaTransformations,\n      },\n      scale: {\n        startValue: 1.3 * scale,\n        transformations: scalingTransformations,\n      },\n    };\n  }\n\n  prepare(setting: Partial<OsuClassicSliderRepeatSettings>): void {\n    this.settings = { ...this.settings, ...setting };\n    const { rotationAngle, texture, position, scale, hit, fadeInOutDuration, approachDuration, beatLength, time } =\n      this.settings;\n\n    this.sprite.anchor.set(0.5);\n    this.sprite.texture = texture;\n    this.sprite.position.set(position.x, position.y);\n    this.sprite.rotation = rotationAngle;\n\n    const props = evaluateTransformationsToProperties(\n      OsuClassicSliderRepeat.normalTransformation({\n        approachDuration,\n        beatLength,\n        fadeInOutDuration,\n        hit,\n        scale,\n      }),\n      time,\n    );\n\n    this.sprite.alpha = props.alpha as number;\n    this.sprite.scale.set(props.scale as number);\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicSliderTick.ts",
    "content": "import {\n  applyPropertiesToDisplayObject,\n  createCenteredSprite,\n  DisplayObjectTransformationProcess,\n  evaluateTransformationsToProperties,\n} from \"../utils/Pixi\";\nimport { Sprite, Texture } from \"pixi.js\";\nimport { Easing, Position } from \"@osujs/math\";\nimport { fadeInT, fadeOutT } from \"../utils/Transformations\";\n\n// Lazer/DrawableSliderTick.cs\n\ninterface SliderTickSettings {\n  time: number;\n  position: Position;\n  scale: number;\n  texture: Texture;\n  approachDuration: number; // Has complicated logic\n  hit?: boolean;\n}\n\nconst ANIM_DURATION = 150;\n\nexport class OsuClassicSliderTick {\n  public readonly sprite: Sprite;\n\n  constructor() {\n    this.sprite = createCenteredSprite();\n  }\n\n  static normalTransformation(settings: {\n    approachDuration: number;\n    hit: boolean;\n  }): DisplayObjectTransformationProcess {\n    const { approachDuration, hit } = settings;\n    const hitTime = 0;\n    return {\n      alpha: {\n        startValue: 0,\n        transformations: [\n          { time: [-approachDuration, -approachDuration + ANIM_DURATION], func: fadeInT() },\n          // TODO: Depending on HIT\n          hit\n            ? { time: [hitTime, hitTime + ANIM_DURATION], func: fadeOutT(0, Easing.OUT_QUINT) }\n            : { time: [hitTime, hitTime + ANIM_DURATION], func: fadeOutT(0) },\n        ],\n      },\n    };\n  }\n\n  prepare(settings: SliderTickSettings): void {\n    const { time, position, scale, texture, approachDuration, hit } = settings;\n\n    this.sprite.texture = texture;\n    this.sprite.position.set(position.x, position.y);\n    this.sprite.scale.set(scale); // TODO: Actually it scales from 0.5 to 1.0\n\n    const t = OsuClassicSliderTick.normalTransformation({ approachDuration, hit: hit ?? false });\n    const p = evaluateTransformationsToProperties(t, time);\n    applyPropertiesToDisplayObject(p, this.sprite);\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hitobjects/OsuClassicSpinner.ts",
    "content": "import { Container, Sprite, Texture } from \"pixi.js\";\nimport { clamp } from \"@osujs/math\";\nimport {\n  applyPropertiesToDisplayObject,\n  DisplayObjectTransformationProcess,\n  evaluateTransformationsToProperties,\n} from \"../utils/Pixi\";\nimport { scaleToT } from \"../utils/Transformations\";\nimport { OSU_PLAYFIELD_HEIGHT, OSU_PLAYFIELD_WIDTH } from \"../utils/constants\";\n\n// New style\n\n// Time = 0 is when it's done and a \"CLEAR\" is shown.\ninterface OsuClassicSpinnerSettings {\n  approachCircleTexture: Texture;\n  modHidden: boolean;\n  duration: number;\n  time: number;\n}\n\nfunction approachCircleTransformation(settings: {\n  modHidden: boolean;\n  duration: number;\n}): DisplayObjectTransformationProcess {\n  const { modHidden, duration } = settings;\n  const hitTime = 0;\n  const spawnTime = hitTime - duration;\n\n  if (modHidden) {\n    return {\n      alpha: {\n        startValue: 0,\n      },\n    };\n  }\n  return {\n    scale: {\n      startValue: 1.0,\n      transformations: [{ time: [spawnTime, hitTime], func: scaleToT(0) }],\n    },\n  };\n}\n\nexport class OsuClassicSpinner {\n  container: Container;\n  approachCircleSprite: Sprite;\n\n  constructor() {\n    this.container = new Container();\n    this.container.addChild((this.approachCircleSprite = new Sprite()));\n    this.approachCircleSprite.anchor.set(0.5, 0.5);\n    this.approachCircleSprite.position.set(OSU_PLAYFIELD_WIDTH / 2, OSU_PLAYFIELD_HEIGHT / 2);\n  }\n\n  prepare(settings: OsuClassicSpinnerSettings) {\n    const { approachCircleTexture, modHidden, duration, time } = settings;\n\n    const finishedPercent = clamp((duration + time) / duration, 0, 1);\n\n    // ApproachCircle\n    this.approachCircleSprite.texture = approachCircleTexture;\n    applyPropertiesToDisplayObject(\n      evaluateTransformationsToProperties(\n        approachCircleTransformation({\n          modHidden,\n          duration,\n        }),\n        time,\n      ),\n      this.approachCircleSprite,\n    );\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hud/HitErrorBar.ts",
    "content": "import { Container, Sprite, Texture } from \"pixi.js\";\nimport { applyInterpolation, Easing, rgbToInt } from \"@osujs/math\";\n\ntype HitEvent = {\n  offset: number;\n  timeAgo: number;\n  miss?: boolean;\n};\n\nexport interface AnalysisHitErrorBarSettings {\n  hitWindow50: number;\n  hitWindow100: number;\n  hitWindow300: number;\n  hits: HitEvent[];\n}\n\n// https://github.com/McKay42/McOsu/commit/b5803cd37dd3c27741b76744b5d141f425e9b33c\nconst color300 = rgbToInt([50, 188, 231]);\nconst color100 = rgbToInt([87, 227, 19]);\nconst color50 = rgbToInt([218, 174, 70]);\nconst colorMiss = rgbToInt([255, 0, 0]);\nconst barHeight = 5;\nconst hitHeight = 15;\n\nfunction coloredSprite(color: number) {\n  const sprite = new Sprite(Texture.WHITE);\n  sprite.tint = color;\n  sprite.anchor.set(0.5);\n\n  return sprite;\n}\n\nexport class OsuClassicHitErrorBar {\n  container: Container;\n  bg50: Sprite;\n  bg100: Sprite;\n  bg300: Sprite;\n  center: Sprite;\n\n  hitsContainer: Container;\n\n  constructor() {\n    this.container = new Container();\n    this.hitsContainer = new Container();\n    this.bg50 = coloredSprite(color50);\n    this.bg100 = coloredSprite(color100);\n    this.bg300 = coloredSprite(color300);\n    this.center = coloredSprite(0xffffff);\n\n    this.bg50.height = this.bg100.height = this.bg300.height = barHeight;\n    this.center.height = hitHeight;\n    this.center.width = 1;\n    this.center.position.set(-0.5, 0);\n\n    this.container.addChild(this.bg50, this.bg100, this.bg300, this.center, this.hitsContainer);\n  }\n\n  prepare(setting: AnalysisHitErrorBarSettings) {\n    const { hitWindow50, hitWindow100, hitWindow300, hits } = setting;\n\n    function colorFromHitEvent(hitEvent: HitEvent) {\n      if (hitEvent.miss) return colorMiss;\n      if (Math.abs(hitEvent.offset) <= hitWindow300) return color300;\n      if (Math.abs(hitEvent.offset) <= hitWindow100) return color100;\n      if (Math.abs(hitEvent.offset) <= hitWindow50) return color50;\n      return colorMiss;\n    }\n\n    this.bg50.width = hitWindow50 * 2;\n    this.bg100.width = hitWindow100 * 2;\n    this.bg300.width = hitWindow300 * 2;\n\n    const maxTime = 10000;\n    this.hitsContainer.removeChildren();\n    hits.forEach((h) => {\n      const s = coloredSprite(colorFromHitEvent(h));\n      const p = applyInterpolation(h.timeAgo, 0, maxTime, 1.0, 0.0, Easing.OUT_QUINT);\n      s.width = 1;\n      s.height = hitHeight * p;\n      s.position.set(h.offset, 0);\n      s.alpha = p;\n      this.hitsContainer.addChild(s);\n    });\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hud/OsuClassicAccuracy.ts",
    "content": "import { Container, Sprite, Texture } from \"pixi.js\";\nimport { OsuClassicNumber } from \"./OsuClassicNumber\";\nimport { calculateAccuracyDigits } from \"../utils/numbers\";\n\ninterface OsuClassicAccuracySettings {\n  // Number between 0 and 1\n  accuracy: number;\n  overlap: number;\n  digitTextures: Texture[];\n  dotTexture: Texture;\n  percentageTexture: Texture; // Note that some skins abuse this texture to shift the whole accuracy to the left.\n}\n\nexport class OsuClassicAccuracy {\n  container: Container;\n  beforeNumber: OsuClassicNumber;\n  afterNumber: OsuClassicNumber;\n  dotSprite: Sprite;\n  percentageSprite: Sprite;\n\n  constructor() {\n    this.beforeNumber = new OsuClassicNumber();\n    this.afterNumber = new OsuClassicNumber();\n    this.dotSprite = new Sprite();\n    this.percentageSprite = new Sprite();\n    this.container = new Container();\n    this.container.addChild(this.beforeNumber, this.dotSprite, this.afterNumber, this.percentageSprite);\n  }\n\n  prepare(settings: OsuClassicAccuracySettings) {\n    const { accuracy, overlap, digitTextures, percentageTexture, dotTexture } = settings;\n    const [beforeDigits, afterDigits] = calculateAccuracyDigits(accuracy);\n    this.beforeNumber.prepare({ digits: beforeDigits, overlap, textures: digitTextures });\n    this.afterNumber.prepare({ digits: afterDigits, overlap, textures: digitTextures });\n    this.dotSprite.texture = dotTexture;\n    this.percentageSprite.texture = percentageTexture;\n\n    let width = 0;\n\n    // Interestingly `overlap` also applies to the the dot and the percentage based on visual testing (not confirmed).\n    for (let i = 0; i < this.container.children.length; i++) {\n      const c = this.container.children[i] as Container;\n      const applyOverlap = i > 0 ? -overlap : 0;\n      c.position.set(width + applyOverlap, 0);\n      width += c.width + applyOverlap;\n    }\n\n    // Shift to left\n    for (let i = 0; i < this.container.children.length; i++) {\n      const c = this.container.children[i] as Container;\n      c.position.set(c.position.x - width, 0);\n    }\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hud/OsuClassicJudgement.ts",
    "content": "import { Sprite, Texture } from \"pixi.js\";\nimport { AnimationTimeSetting, PositionSetting, ScaleSetting } from \"../DrawableSettings\";\nimport { Easing, Position } from \"@osujs/math\";\nimport { PrepareSetting } from \"../utils/Preparable\";\nimport {\n  applyPropertiesToDisplayObject,\n  DisplayObjectTransformationProcess,\n  evaluateTransformationsToProperties,\n} from \"../utils/Pixi\";\nimport { fadeOutT, scaleToT } from \"../utils/Transformations\";\nimport { animationIndex } from \"../utils/Animation\";\n\nexport interface OsuClassicJudgementsSettings extends AnimationTimeSetting, PositionSetting, ScaleSetting {\n  time: number;\n  position: Position;\n  scale: number;\n\n  // Example: 60fps (16.6667ms each frame) and two textures given\n  // At time=0ms texture[0], at 17ms texture[1] will be used at 34ms texture[1] will still be used since it's the last\n  // one. In case there is no animation just provide a list of one texture.\n  textures: Texture[];\n  animationFrameRate: number;\n\n  // TODO\n  // Miss judgement has way different animation -> it slides down a bit.\n  // In osu!lazer there is even a slight rotation\n  // isMiss: boolean;\n}\n\nconst defaultSettings: OsuClassicJudgementsSettings = {\n  time: 0,\n  position: { x: 0, y: 0 },\n  scale: 0.57, // cs4\n  textures: [Texture.EMPTY],\n  animationFrameRate: 60,\n};\n\n//\n/**\n * The judgements are sprites that represent Miss, 50, 100, 300, 100-genki, 300-genki.\n *\n * Animation starts at time=0 and should be included at the time when the has hit the circle or missed.\n */\nexport class OsuClassicJudgement implements PrepareSetting<OsuClassicJudgementsSettings> {\n  sprite: Sprite;\n  settings: OsuClassicJudgementsSettings;\n\n  constructor() {\n    this.sprite = new Sprite();\n    this.sprite.anchor.set(0.5, 0.5);\n    this.settings = defaultSettings;\n  }\n\n  // TODO: Miss animation\n  static normalTransformation(scale: number, position: Position): DisplayObjectTransformationProcess {\n    return {\n      alpha: {\n        startValue: 1,\n        transformations: [{ time: [0, 1000], func: fadeOutT() }],\n      },\n      scale: {\n        startValue: scale * 0.9,\n        transformations: [{ time: [0, 500], func: scaleToT(scale * 1.0, Easing.OUT_ELASTIC) }],\n      },\n      position: {\n        startValue: position,\n      },\n    };\n  }\n\n  prepare(setting: Partial<OsuClassicJudgementsSettings>) {\n    this.settings = { ...this.settings, ...setting };\n\n    const { position, scale, textures, time, animationFrameRate } = this.settings;\n\n    // Last one will be taken in case there are none left...\n    if (textures.length > 0) {\n      const idx = Math.min(textures.length - 1, animationIndex(time, animationFrameRate));\n      this.sprite.texture = textures[idx];\n    } else {\n      this.sprite.texture = Texture.EMPTY;\n    }\n\n    const props = evaluateTransformationsToProperties(OsuClassicJudgement.normalTransformation(scale, position), time);\n    applyPropertiesToDisplayObject(props, this.sprite);\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/hud/OsuClassicNumber.ts",
    "content": "import { Container, Sprite, Texture } from \"pixi.js\";\n\nexport type OsuClassicNumberSettings = {\n  digits: number[];\n  textures: Texture[];\n  overlap: number;\n};\n\nexport const DEFAULT_NUMBER_TEXTURES: Texture[] = Array(10).fill(Texture.EMPTY);\n\nconst DEFAULT_NUMBER_SETTINGS: OsuClassicNumberSettings = {\n  digits: [0],\n  textures: DEFAULT_NUMBER_TEXTURES,\n  overlap: 0,\n};\n\n/**\n * This number can be used for hitCircleCombo, currentCombo and score\n * TODO: Do not extend from Container\n */\nexport class OsuClassicNumber extends Container {\n  private lastNumber: number[] = [];\n  private settings: OsuClassicNumberSettings;\n\n  constructor() {\n    super();\n    this.settings = DEFAULT_NUMBER_SETTINGS;\n  }\n\n  // Trick\n  // https://github.com/pixijs/pixijs/issues/3272#issuecomment-349359529\n  _anchorX = 0;\n  _anchorY = 0;\n\n  set anchorX(value) {\n    this._anchorX = value;\n    this.pivot.x = (value * this.width) / this.scale.x;\n  }\n\n  get anchorX() {\n    return this._anchorX;\n  }\n\n  set anchorY(value) {\n    this._anchorY = value;\n    this.pivot.y = (value * this.height) / this.scale.y;\n  }\n\n  get anchorY() {\n    return this._anchorY;\n  }\n\n  prepare(settings: OsuClassicNumberSettings): void {\n    this.settings = Object.freeze({ ...this.settings, ...settings });\n\n    const { digits, overlap, textures } = this.settings;\n\n    // TODO: Currently we only cache by number, but need to change with other params as well, otherwise we won't see\n    // any changes if the overlap or the textures change\n    // MAybe instead of Number pass araray of digits\n    if (this.lastNumber === digits) {\n      return;\n    }\n\n    this.lastNumber = digits;\n    this.children.forEach((t) => t.destroy(true));\n    this.removeChildren();\n\n    let totalWidth = 0;\n    const digitSprites: Sprite[] = [];\n\n    for (let i = 0; i < digits.length; i++) {\n      const digit = digits[i];\n      const texture = textures[digit];\n      const sprite = new Sprite(texture);\n      const addOverlap = i > 0 ? -overlap : 0; //\n      sprite.anchor.set(0, 0);\n      sprite.position.set(totalWidth + addOverlap, 0);\n      totalWidth += texture.width + addOverlap;\n      digitSprites.push(sprite);\n    }\n    // Now shift everything to the center since we started from 0 and worked our way to totalWidth\n    // digitSprites.forEach((sprite) => sprite.position.set(sprite.position.x - totalWidth / 2, 0));\n    this.addChild(...digitSprites);\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/index.ts",
    "content": "export * from \"./playfield/PlayfieldBorder\";\n\nexport * from \"./hitobjects/OsuClassicHitCircleArea\";\nexport * from \"./hitobjects/OsuClassicApproachCircle\";\nexport * from \"./hitobjects/OsuClassicSliderBody\";\nexport * from \"./hitobjects/OsuClassicSliderRepeat\";\nexport * from \"./hitobjects/OsuClassicSliderBall\";\nexport * from \"./hitobjects/OsuClassicSliderTick\";\nexport * from \"./hitobjects/OsuClassicCursor\";\nexport * from \"./hitobjects/OsuClassicSpinner\";\n\nexport * from \"./hud/HitErrorBar\";\nexport * from \"./hud/OsuClassicNumber\";\nexport * from \"./hud/OsuClassicAccuracy\";\nexport * from \"./hud/OsuClassicJudgement\";\n\nexport * from \"./DrawableSettings\";\n\nexport * from \"./renderers/BasicSliderTextureRenderer\";\nexport { calculateDigits } from \"./utils/numbers\";\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/playfield/PlayfieldBorder.ts",
    "content": "import { Graphics } from \"pixi.js\";\nimport { circleSizeToScale } from \"@osujs/math\";\nimport { OSU_PLAYFIELD_HEIGHT, OSU_PLAYFIELD_WIDTH } from \"../utils/constants\";\n\nexport interface PlayfieldBorderSettings {\n  // Boolean flag\n  enabled: boolean;\n  // Thickness in osu!px\n  thickness: number;\n\n  // Other suggestions:\n  // alpha: number;\n  // color: number;\n  // type : \"full\" | \"corners\" | ...;\n  // scaleWithCS: boolean; // Currently it adjusts to CS4.\n}\n\nconst cs4 = circleSizeToScale(4) * 64;\nconst DEFAULT_COLOR = 0xffffff;\nconst DEFAULT_ALPHA = 0.7;\n\nexport class PlayfieldBorder {\n  graphics: Graphics;\n\n  constructor() {\n    this.graphics = new Graphics();\n  }\n\n  onSettingsChange(settings: PlayfieldBorderSettings) {\n    const { thickness, enabled } = settings;\n\n    this.graphics.clear();\n\n    if (enabled) {\n      this.graphics.lineStyle(thickness, DEFAULT_COLOR, DEFAULT_ALPHA);\n      const offsetX = cs4;\n      const offsetY = cs4 / (4.0 / 3);\n\n      this.graphics.drawRect(-offsetX, -offsetY, OSU_PLAYFIELD_WIDTH + offsetX * 2, OSU_PLAYFIELD_HEIGHT + offsetY * 2);\n    }\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/renderers/BasicSliderTextureRenderer.ts",
    "content": "import * as PIXI from \"pixi.js\";\nimport { MeshMaterial, RenderTexture, Shader } from \"pixi.js\";\nimport { Bounds } from \"@pixi/display\";\nimport { Position, Vec2 } from \"@osujs/math\";\n\n// TODO: maybe change to -1 like in osu!lazer\n// McOsu uses 0.5\nconst MESH_CENTER_HEIGHT = -0.1;\nconst SLIDER_BODY_UNIT_CIRCLE_SUBDIVISIONS = 42;\n\nconst vertexShader = `\nprecision mediump float;\n\nattribute vec3 position;\nattribute vec2 uv;\n\n\n// Uniforms inserted by pixi.js\nuniform mat3 translationMatrix;\nuniform mat3 projectionMatrix;\n\nvarying vec2 v_uv;\n\nvoid main()\n{\n  v_uv = uv;\n  gl_Position = vec4((projectionMatrix * translationMatrix * vec3(position.xy, 0.0)).xy, position.z, 1.0);\n}\n`;\nconst fragmentShader = `\nprecision mediump float;\n\nuniform float bodyColorSaturation;\nuniform float bodyAlphaMultiplier;\nuniform float borderSizeMultiplier;\nuniform vec3 colBorder;\nuniform vec3 colBody;\n\nvarying vec2 v_uv;\n\nconst float defaultTransitionSize = 0.011;\nconst float defaultBorderSize = 0.11;\nconst float outerShadowSize = 0.08;\n\nvec4 getInnerBodyColor(in vec4 bodyColor)\n{\n    float brightnessMultiplier = 0.25;\n    bodyColor.r = min(1.0, bodyColor.r * (1.0 + 0.5 * brightnessMultiplier) + brightnessMultiplier);\n    bodyColor.g = min(1.0, bodyColor.g * (1.0 + 0.5 * brightnessMultiplier) + brightnessMultiplier);\n    bodyColor.b = min(1.0, bodyColor.b * (1.0 + 0.5 * brightnessMultiplier) + brightnessMultiplier);\n    return vec4(bodyColor);\n}\n\nvec4 getOuterBodyColor(in vec4 bodyColor)\n{\n    float darknessMultiplier = 0.1;\n    bodyColor.r = min(1.0, bodyColor.r / (1.0 + darknessMultiplier));\n    bodyColor.g = min(1.0, bodyColor.g / (1.0 + darknessMultiplier));\n    bodyColor.b = min(1.0, bodyColor.b / (1.0 + darknessMultiplier));\n    return vec4(bodyColor);\n}\n\nvoid main()\n{\n    float borderSize = defaultBorderSize*borderSizeMultiplier;\n    const float transitionSize = defaultTransitionSize;\n\n    // output var\n    vec4 outColor = vec4(0.0);\n\n    // dynamic color calculations\n    vec4 borderColor = vec4(colBorder.x, colBorder.y, colBorder.z, 1.0);\n    vec4 bodyColor = vec4(colBody.x, colBody.y, colBody.z, 0.7*bodyAlphaMultiplier);\n    vec4 outerShadowColor = vec4(0, 0, 0, 0.25);\n    vec4 innerBodyColor = getInnerBodyColor(bodyColor);\n    vec4 outerBodyColor = getOuterBodyColor(bodyColor);\n\n    innerBodyColor.rgb *= bodyColorSaturation;\n    outerBodyColor.rgb *= bodyColorSaturation;\n\n    // a bit of a hack, but better than rough edges\n    if (borderSizeMultiplier < 0.01)\n    borderColor = outerShadowColor;\n\n    // conditional variant\n\n    if (v_uv.x < outerShadowSize - transitionSize) // just shadow\n    {\n        float delta = v_uv.x / (outerShadowSize - transitionSize);\n        outColor = mix(vec4(0), outerShadowColor, delta);\n    }\n    if (v_uv.x > outerShadowSize - transitionSize && v_uv.x < outerShadowSize + transitionSize) // shadow + border\n    {\n        float delta = (v_uv.x - outerShadowSize + transitionSize) / (2.0*transitionSize);\n        outColor = mix(outerShadowColor, borderColor, delta);\n    }\n    if (v_uv.x > outerShadowSize + transitionSize && v_uv.x < outerShadowSize + borderSize - transitionSize) // just border\n    {\n        outColor = borderColor;\n    }\n    if (v_uv.x > outerShadowSize + borderSize - transitionSize && v_uv.x < outerShadowSize + borderSize + transitionSize) // border + outer body\n    {\n        float delta = (v_uv.x - outerShadowSize - borderSize + transitionSize) / (2.0*transitionSize);\n        outColor = mix(borderColor, outerBodyColor, delta);\n    }\n    if (v_uv.x > outerShadowSize + borderSize + transitionSize) // outer body + inner body\n    {\n        float size = outerShadowSize + borderSize + transitionSize;\n        float delta = ((v_uv.x - size) / (1.0-size));\n        outColor = mix(outerBodyColor, innerBodyColor, delta);\n    }\n\n    gl_FragColor = outColor;\n}\n`;\n\n// because Math.sin and Math.cos are slow ... probably\nfunction getUnitCircle(numberOfDivisions: number): Position[] {\n  const unitCircle: Position[] = [];\n  for (let i = 0; i < numberOfDivisions; i++) {\n    const phase = (i * Math.PI * 2) / numberOfDivisions;\n    const pointOnCircle = { x: Math.sin(phase), y: Math.cos(phase) };\n    unitCircle.push(pointOnCircle);\n  }\n  return unitCircle;\n}\n\nfunction getUnitCircleScaled(numberOfDivisions: number, radius: number) {\n  const unitCircle = getUnitCircle(numberOfDivisions);\n  return unitCircle.map((u: Position) => Vec2.scale(u, radius));\n}\n\n// Actually ~65536\n// But maybe I did some Math wrong...\nconst MAX_NUMBER_OF_VERTICES = 60000;\n\nfunction numberOfVertices(numberOfPoints: number) {\n  // Cap + Joins\n  return numberOfPoints * (SLIDER_BODY_UNIT_CIRCLE_SUBDIVISIONS + 1) + (numberOfPoints - 1) * 6;\n}\n\nexport function getSliderGeometry(points: Position[], radius: number): PIXI.Geometry {\n  const coneNumberOfDivisions = SLIDER_BODY_UNIT_CIRCLE_SUBDIVISIONS;\n  const numberOfVerticesEachCapJoin = coneNumberOfDivisions + 1;\n  const numberOfVerticesEachSegment = 6;\n  const meshCenterHeight = MESH_CENTER_HEIGHT;\n\n  const positionNumComponents = 3;\n  const uvNumComponents = 2;\n\n  const numberOfCaps = 2;\n  const numberOfJoins = points.length - 2;\n  const numberOfSegments = points.length - 1;\n\n  const offsetCaps = 0;\n  const offsetJoins = numberOfCaps * numberOfVerticesEachCapJoin;\n  const offsetSegments = offsetJoins + numberOfJoins * numberOfVerticesEachCapJoin;\n  const total = offsetSegments + 6 * numberOfSegments;\n\n  // We use TypedArrays because we can then change them (see threejsfundamentals.org on BufferedGeometry)\n  // x,y,z\n  const attrVertices = new Float32Array(positionNumComponents * total);\n  // u,v\n  const attrTextureCoords = new Float32Array(uvNumComponents * total);\n\n  // for (let i = 0; i < total; i++) { attrVertices.set([0, 0, 0], i * 3); attrTextureCoords.set([0, 0], i * 2); }\n\n  const getCapsOffset = (i: number) => offsetCaps + i * numberOfVerticesEachCapJoin;\n  const getOffsetJoins = (i: number) => offsetJoins + i * numberOfVerticesEachCapJoin;\n  const getOffsetSegments = (i: number) => offsetSegments + i * 6;\n\n  const indices: number[] = [];\n\n  const unitCircleScaled = getUnitCircleScaled(coneNumberOfDivisions, radius);\n\n  function addLineCap(position: Position, offset: number) {\n    // Tip of the cone\n    attrVertices.set([position.x, position.y, meshCenterHeight], offset * positionNumComponents);\n    attrTextureCoords.set([1, 0], offset * uvNumComponents);\n    // Other points at the bottom\n    for (let i = 0; i < coneNumberOfDivisions; i++) {\n      const p = Vec2.add(position, unitCircleScaled[i]);\n      // console.log(p, unitCircleScaled[i]);\n      attrVertices.set([p.x, p.y, 0], (offset + 1 + i) * positionNumComponents);\n      attrTextureCoords.set([0, 0], (offset + 1 + i) * uvNumComponents);\n    }\n    // Now add indices to draw this mesh. It's always a triangle from the top of the cone to a small segment along\n    // the circle. This also handles the wrapping case (actually just the last one).\n    for (let i = 0; i < coneNumberOfDivisions; i++) {\n      // indices.push(offset, offset + 1 + i, offset + 1 + (i + 1) % coneNumberOfDivisions);\n      indices.push(offset, offset + 1 + ((i + 1) % coneNumberOfDivisions), offset + 1 + i);\n    }\n  }\n\n  const nPoints = points.length;\n\n  // Beginning Cap\n  addLineCap(points[0], getCapsOffset(0));\n  // End Cap\n  addLineCap(points[nPoints - 1], getCapsOffset(1));\n  // Draw the joints\n  for (let i = 1; i < nPoints - 1; i++) {\n    addLineCap(points[i], getOffsetJoins(i - 1));\n  }\n\n  // Draw the segments (card pyramid bottom)\n  // type is triangles, build a rectangle out of 6 vertices\n  //\n  // 1   3   5\n  // *---*---*\n  // |  /|  /|\n  // | / | / |     // the line 3-4 is the center of the slider (with a raised z-coordinate for blending)\n  // |/  |/  |\n  // *---*---*\n  // 2   4   6\n  //\n  // Imagine 1 2 3 4 as the one card and 3 4 5 6 as the other card in a mini card pyramid.\n  // The slider goes from 4 (start) to 3 (end)\n\n  const quadsIndices = [1, 2, 3, 3, 2, 4, 3, 4, 5, 5, 4, 6];\n\n  function addQuads(lineStartPoint: Position, lineEndPoint: Position, offset: number) {\n    const lineDirectionNormalized = Vec2.sub(lineEndPoint, lineStartPoint).normalized();\n    const lineOrthogonalDirection = new Vec2(-lineDirectionNormalized.y, lineDirectionNormalized.x);\n    const orthoScaled = lineOrthogonalDirection.scale(radius);\n\n    // In McOsu we have .add and .sub swapped and it didn't get rendered due to culling.\n\n    // Point 1 (=screenLineLeftEndPoint)\n    const p1 = Vec2.add(lineEndPoint, orthoScaled);\n    attrVertices.set([p1.x, p1.y, 0], (offset + 0) * positionNumComponents);\n    attrTextureCoords.set([0, 0], (offset + 0) * uvNumComponents);\n\n    // Point 2 (=screenLineLeftStartPoint)\n    const p2 = Vec2.add(lineStartPoint, orthoScaled);\n    attrVertices.set([p2.x, p2.y, 0], (offset + 1) * positionNumComponents);\n    attrTextureCoords.set([0, 0], (offset + 1) * uvNumComponents);\n\n    // Point 3 (=screenLineEndPoint)\n    const p3 = lineEndPoint;\n    attrVertices.set([p3.x, p3.y, meshCenterHeight], (offset + 2) * positionNumComponents);\n    attrTextureCoords.set([1, 0], (offset + 2) * uvNumComponents);\n\n    // Point 4 (=screenLineStartPoint)\n    const p4 = lineStartPoint;\n    attrVertices.set([p4.x, p4.y, meshCenterHeight], (offset + 3) * positionNumComponents);\n    attrTextureCoords.set([1, 0], (offset + 3) * uvNumComponents);\n\n    // Point 5 (=screenLineRightEndPoint)\n    const p5 = Vec2.sub(lineEndPoint, orthoScaled);\n    attrVertices.set([p5.x, p5.y, 0], (offset + 4) * positionNumComponents);\n    attrTextureCoords.set([0, 0], (offset + 4) * uvNumComponents);\n\n    // Point 6 (=screenLineRightStartPoint)\n    const p6 = Vec2.sub(lineStartPoint, orthoScaled);\n    attrVertices.set([p6.x, p6.y, 0], (offset + 5) * positionNumComponents);\n    attrTextureCoords.set([0, 0], (offset + 5) * uvNumComponents);\n\n    const a = quadsIndices.map((x) => offset + (x - 1));\n    indices.push(...a);\n  }\n\n  for (let i = 1; i < nPoints; i++) {\n    addQuads(points[i - 1], points[i], getOffsetSegments(i - 1));\n  }\n\n  const geometry = new PIXI.Geometry();\n\n  geometry.addAttribute(\"position\", PIXI.Buffer.from(attrVertices), positionNumComponents);\n  geometry.addAttribute(\"uv\", PIXI.Buffer.from(attrTextureCoords), uvNumComponents);\n  geometry.addIndex(indices);\n\n  // I don't think changing the attributes of a geometry in PIXI.js is possible.\n  // If we need a new geometry, we should generate a new one.\n\n  // Some notes for when I used to render a slider with Three.JS:\n  // Important for some optimization (giving a hint to the GPU on how it should organize the data).\n  // positionAttribute.setUsage(DynamicDrawUsage);\n  // Later we can update the positions and just update:\n  // attrVertices.set([3,4,5], 1);\n  // positionAttribute.needsUpdate = true;\n  // The only thing we are sending again to the GPU is the `positionAttribute`... I THINK\n\n  return geometry;\n}\n\nconst defaultUniforms = {\n  bodyColorSaturation: 1.0,\n  bodyAlphaMultiplier: 1.0,\n  borderSizeMultiplier: 1.0,\n  colBorder: [1, 1, 1],\n  colBody: [0, 0, 0],\n};\n\ntype Color = [number, number, number];\ntype SliderTextureParams = {\n  resolution: number;\n\n  // the points of the slider path (if you use snaking, then give a subset of points with interpolated endings ofc)\n  path: Position[];\n  // basically determined by CircleSize\n  radius: number;\n\n  // Colors should be given between 0 and 256.\n  borderColor?: Color;\n  bodyColor?: Color;\n};\n\n// [0, 255] -> [0, 1] mapping\nconst rgbNormalized = (color: Color) => color.map((c) => c / 256.0);\n\nexport const getBoundsFromSliderPath = (points: Position[], radius: number): Bounds => {\n  const bounds = new Bounds();\n  points.forEach((p) => bounds.addPoint(p));\n  bounds.minX -= radius;\n  bounds.minY -= radius;\n  bounds.maxX += radius;\n  bounds.maxY += radius;\n  return bounds;\n};\n\n// Does no caching, just returns a Texture as requested.\n// Shader is only initialized once, since we can just change the uniforms for changing slider border color.\n/**\n * In case of very large sliders such as the one in The Sun The Moon The Star (~10:40) that has over 100k vertices\n * we need to make multiple render calls because the index buffer is a uint16 array (due to compatibility reasons)\n * and we can thus not refer to indices larger than ~65k.\n *\n * https://stackoverflow.com/questions/4998278/is-there-a-limit-of-vertices-in-webgl\n */\nexport class BasicSliderTextureRenderer {\n  shader: PIXI.Shader;\n  renderer: PIXI.Renderer;\n\n  constructor(renderer: PIXI.Renderer) {\n    this.renderer = renderer;\n    this.shader = Shader.from(vertexShader, fragmentShader, defaultUniforms);\n  }\n\n  render(params: SliderTextureParams): RenderTexture {\n    const { bodyColor, borderColor, resolution, path, radius } = params;\n\n    if (borderColor) this.shader.uniforms.colBorder = rgbNormalized(borderColor);\n    if (bodyColor) this.shader.uniforms.colBody = rgbNormalized(bodyColor);\n\n    const bounds = getBoundsFromSliderPath(path, radius);\n    const { minX, maxX, minY, maxY } = bounds;\n\n    const width = maxX - minX;\n    const height = maxY - minY;\n\n    // TODO: do some pooling here\n    const renderTexture = RenderTexture.create({ width, height, resolution });\n    renderTexture.framebuffer.enableDepth();\n\n    const boundaryBoxCenter = new Vec2((maxX + minX) / 2, (maxY + minY) / 2);\n    // The reason why we shift the points is that a frame buffer of size [w, h] will only render the points with the\n    // coordinates in Rectangle{(-w/2,-h/2), (+w/2,+h/2)}. Currently some points may be outside this rectangle,\n    // that's why we have to shift them to the center.\n    const points = path.map((p) => Vec2.sub(p, boundaryBoxCenter));\n\n    const renderSubPath = (points: Vec2[]) => {\n      const geometry = getSliderGeometry(points, radius);\n      const sliderMesh = new PIXI.Mesh(geometry, this.shader as MeshMaterial);\n      sliderMesh.state.depthTest = true;\n      sliderMesh.state.blend = false;\n      // Or maybe even use Transform here\n      this.renderer.render(sliderMesh, { renderTexture, clear: false });\n    };\n\n    // The reason we have to render the whole slider in multiple sub-paths is described above.\n    // Usually this will only happen for long sliders (in terms of vertices).\n    let subPoints: Vec2[] = [];\n    for (let i = 0; i < points.length; i++) {\n      if (numberOfVertices(subPoints.length + 1) > MAX_NUMBER_OF_VERTICES) {\n        renderSubPath(subPoints);\n        subPoints = [];\n      }\n      subPoints.push(points[i]);\n    }\n    if (subPoints.length > 0) {\n      renderSubPath(subPoints);\n    }\n\n    // TODO: Do we need to cleanup? disableDepth?\n\n    return renderTexture;\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/utils/Animation.ts",
    "content": "export function animationIndex(time: number, animationFrameRate = 60) {\n  const eachFrameMs = 1000 / animationFrameRate;\n  return Math.floor(time / eachFrameMs);\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/utils/Pixi.ts",
    "content": "import { Sprite } from \"@pixi/sprite\";\nimport { DisplayObject } from \"@pixi/display\";\nimport { evaluateTransformations, TransformationProcess } from \"./Transformations\";\nimport { Texture } from \"pixi.js\";\nimport { applyInterpolation, Easing } from \"@osujs/math\";\n\nexport const createCenteredSprite = (): Sprite => {\n  const sprite = new Sprite();\n  sprite.anchor.set(0.5);\n  return sprite;\n};\n\nexport function setAlphaScaling(o: DisplayObject, alpha: number, scaling: number): void {\n  o.alpha = alpha;\n  o.scale.set(scaling);\n}\n\n/** TRANSFORMATIONS **/\n// Animations [from, to)\n// And boolean returns if a change was done\n\nexport function fading(\n  o: DisplayObject,\n  t: number,\n  fromTime: number,\n  toTime: number,\n  startAlpha: number,\n  endAlpha: number,\n  easing = Easing.LINEAR,\n): boolean {\n  // toTime is exclusive\n  if (fromTime <= t && t < toTime) {\n    o.alpha = applyInterpolation(t, fromTime, toTime, startAlpha, endAlpha, easing);\n    return true;\n  } else if (toTime <= t) {\n    o.alpha = endAlpha;\n  } else {\n    o.alpha = startAlpha;\n  }\n  return false;\n}\n\nexport function fadeInFromTo(o: DisplayObject, t: number, from: number, to: number, easing = Easing.LINEAR): boolean {\n  return fading(o, t, from, to, 0, 1, easing);\n}\n\nexport function fadeInWithDuration(\n  o: DisplayObject,\n  t: number,\n  from: number,\n  duration: number,\n  easing = Easing.LINEAR,\n): boolean {\n  return fadeInFromTo(o, t, from, from + duration, easing);\n}\n\nexport function fadeOutFromTo(o: DisplayObject, t: number, from: number, to: number, easing = Easing.LINEAR): boolean {\n  return fading(o, t, from, to, 1, 0, easing);\n}\n\nexport function fadeOutWithDuration(\n  o: DisplayObject,\n  t: number,\n  from: number,\n  duration: number,\n  easing = Easing.LINEAR,\n): boolean {\n  return fadeOutFromTo(o, t, from, from + duration, easing);\n}\n\nexport function scaling(\n  o: DisplayObject,\n  t: number,\n  fromTime: number,\n  toTime: number,\n  startVal: number,\n  endVal: number,\n  easing = Easing.LINEAR,\n): boolean {\n  // toTime is exclusive\n  if (fromTime <= t && t < toTime) {\n    o.scale.set(applyInterpolation(t, fromTime, toTime, startVal, endVal, easing));\n    return true;\n  } else if (toTime <= t) {\n    o.scale.set(endVal);\n  } else {\n    o.scale.set(startVal);\n  }\n  return false;\n}\n\nexport function scaleTo(\n  o: DisplayObject,\n  t: number,\n  fromTime: number,\n  toTime: number,\n  endScaling: number,\n  easing = Easing.LINEAR,\n): boolean {\n  return scaling(o, t, fromTime, toTime, 1, endScaling, easing);\n}\n\nexport function scaleToWithDuration(\n  o: DisplayObject,\n  t: number,\n  fromTime: number,\n  duration: number,\n  endScaling: number,\n  easing = Easing.LINEAR,\n): boolean {\n  return scaleTo(o, t, fromTime, fromTime + duration, endScaling, easing);\n}\n\n// PIXI specific\nexport type DisplayObjectTransformationProcess = {\n  alpha?: TransformationProcess<number>;\n  scale?: TransformationProcess<number>;\n  position?: TransformationProcess<{ x: number; y: number }>;\n};\n\ntype AssignableDisplayObjectProperties = {\n  alpha?: number;\n  scale?: number; // could also do \"number | {x, y}\"\n  position?: { x: number; y: number };\n};\n\ntype DisplayObjectProperties = {\n  alpha?: number;\n  scale?: number; // could also do \"number | {x, y}\"\n  position?: { x: number; y: number };\n};\n\ntype AssignableSpriteProperties = AssignableDisplayObjectProperties & {\n  tint?: number;\n  texture?: Texture;\n};\n\n// Side effects from pixi must be considered.\n// We can't just do something like `{...displayObject, ...props}` because the `.set()` methods has had effects such as changing other variables.\n// For instance `displayObject.scale.set(x, y)` will also adjust the `height`, `width`, ...\nexport function applyPropertiesToDisplayObject(\n  props: AssignableDisplayObjectProperties,\n  displayObject: DisplayObject,\n): void {\n  const { alpha, scale, position } = props;\n\n  // Can't check with `if(alpha)` because alpha could be zero and so on, that was a bug in a prior implementation.\n  if (alpha !== undefined) displayObject.alpha = alpha;\n  if (scale !== undefined) displayObject.scale.set(scale);\n  if (position !== undefined) displayObject.position.set(position.x, position.y);\n}\n\nexport const evaluateTransformationsToProperties = (\n  process: DisplayObjectTransformationProcess,\n  time: number,\n  fallbackProperties?: DisplayObjectProperties,\n): AssignableDisplayObjectProperties => {\n  const { alpha, scale, position } = process;\n  return {\n    alpha: alpha !== undefined ? evaluateTransformations(alpha)(time) : fallbackProperties?.alpha,\n    scale: scale !== undefined ? evaluateTransformations(scale)(time) : fallbackProperties?.scale,\n    position: position !== undefined ? evaluateTransformations(position)(time) : fallbackProperties?.position,\n  };\n};\n\nexport const applyTransformationsToDisplayObject =\n  (process: DisplayObjectTransformationProcess) => (displayObject: DisplayObject) => (time: number) => {\n    const { alpha, position, scale } = process;\n    if (alpha) displayObject.alpha = evaluateTransformations(alpha)(time);\n    if (scale) displayObject.scale.set(evaluateTransformations(scale)(time));\n    if (position) {\n      const { x, y } = evaluateTransformations(position)(time);\n      displayObject.position.set(x, y);\n    }\n    return displayObject;\n  };\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/utils/Preparable.ts",
    "content": "export interface PrepareSetting<T> {\n  prepare(setting: Partial<T>): void;\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/utils/Transformations.ts",
    "content": "// Functional programming is sometimes so neat.\n\nimport { applyEasing, clamp, Easing } from \"@osujs/math\";\n\nexport const easingFunction =\n  (easing: Easing) =>\n  (t: number): number =>\n    applyEasing(t, easing);\n\n// We need one for Vector2 as well\nexport const clampedInterpolationFunction =\n  (startTime: number, endTime: number, valA: number, valB: number, easing = Easing.LINEAR) =>\n  (time: number): number => {\n    if (startTime >= endTime) {\n      return valB; // Technically speaking the result is undefined\n    } else {\n      const p = easingFunction(easing)(clamp((time - startTime) / (endTime - startTime), 0, 1));\n      return (valB - valA) * p + valA;\n    }\n  };\n\n// yes time independent\nexport const instantAssignValueTo =\n  <T>(atTime: number, val: T) =>\n  (time: number): T =>\n    val;\n\n/// HANDY ALIASES\n\ntype FunctionOverTime<T> = (time: number) => T;\n\nexport const numericTransformTo =\n  (endValue: number, easing = Easing.LINEAR) =>\n  (startValue: number, fromTime: number, toTime: number): FunctionOverTime<number> =>\n    clampedInterpolationFunction(fromTime, toTime, startValue, endValue, easing);\n\nexport const fadeTo = (endAlpha: number, easing = Easing.LINEAR) => numericTransformTo(endAlpha, easing);\nexport const fadeInT = (endAlpha = 1, easing = Easing.LINEAR) => fadeTo(endAlpha, easing);\nexport const fadeOutT = (endAlpha = 0, easing = Easing.LINEAR) => fadeTo(endAlpha, easing);\n\nexport const scaleToT = (endScaling: number, easing = Easing.LINEAR) => numericTransformTo(endScaling, easing);\n\nexport type Transformation<T> = {\n  time: [number, number];\n  func: (startValue: T, fromTime: number, toTime: number) => FunctionOverTime<T>;\n};\n\n// I'm bad at naming...\nexport type TransformationProcess<T> = {\n  startValue: T;\n  transformations?: Transformation<T>[];\n};\n\n// Assuming that the transformations are sorted and they are not intersecting\nexport const evaluateTransformations =\n  <T>(process: TransformationProcess<T>) =>\n  (time: number): T => {\n    const { startValue, transformations } = process;\n    // Yes, I could also return evaluateTransformation(transformations[1..]) to make it more FP-ish...\n    let value = startValue;\n    if (!transformations) {\n      return value;\n    }\n    for (let i = 0; i < transformations.length; i++) {\n      const [a, b] = transformations[i].time;\n      if (time <= a) return value;\n      // b < time could occur, but in this case, we will have clamping coming in handy\n      value = transformations[i].func(value, a, b)(time);\n    }\n    return value;\n  };\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/utils/constants.ts",
    "content": "// Default field size\nexport const OSU_PLAYFIELD_WIDTH = 512;\nexport const OSU_PLAYFIELD_HEIGHT = 384;\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/utils/numbers.spec.ts",
    "content": "import { calculateAccuracyDigits, calculateDigits } from \"./numbers\";\n\ntest(\"normal digits\", () => {\n  expect(calculateDigits(0)).toEqual([0]);\n  expect(calculateDigits(1)).toEqual([1]);\n  expect(calculateDigits(123)).toEqual([1, 2, 3]);\n});\n\ntest(\"acc digits\", () => {\n  expect(calculateAccuracyDigits(0.123)).toEqual([\n    [1, 2],\n    [3, 0],\n  ]);\n  expect(calculateAccuracyDigits(0.0)).toEqual([[0], [0, 0]]);\n  expect(calculateAccuracyDigits(1.0)).toEqual([\n    [1, 0, 0],\n    [0, 0],\n  ]);\n\n  // Should round down\n  expect(calculateAccuracyDigits(0.12349)).toEqual([\n    [1, 2],\n    [3, 4],\n  ]);\n\n  expect(calculateAccuracyDigits(0.9999999)).toEqual([\n    [9, 9],\n    [9, 9],\n  ]);\n\n  expect(calculateAccuracyDigits(0.0001)).toEqual([[0], [0, 1]]);\n\n  const a = 0.9875776397515528;\n  expect(calculateAccuracyDigits(0.9875776397515528)).toEqual([\n    [9, 8],\n    [7, 5],\n    // [7, 6], In stable it's 98.76% OK\n  ]);\n});\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/src/utils/numbers.ts",
    "content": "/**\n * Returns a list of digits for the given number.\n * For example x = 123 returns [1, 2, 3].\n * For x = 0 it will return [0]\n * @param x a non negative integer\n */\nexport function calculateDigits(x: number) {\n  if (x < 0) {\n    throw Error(\"Only non negative numbers allowed\");\n  }\n  if (x === 0) {\n    return [0];\n  }\n  const d: number[] = [];\n  while (x > 0) {\n    d.push(x % 10);\n    x = Math.floor(x / 10);\n  }\n  d.reverse();\n  return d;\n}\n\n/**\n *\n * @param accuracy\n */\nexport function calculateAccuracyDigits(accuracy: number): [number[], number[]] {\n  if (accuracy < 0 || accuracy > 1) {\n    throw Error(\"Accuracy not between 0 and 1\");\n  }\n\n  // Same as FormatUtils.cs\n  const consideredAcc = Math.floor(accuracy * 10000);\n  const digits = calculateDigits(consideredAcc);\n\n  if (digits.length === 1) {\n    return [[0], [0, digits[0]]];\n  }\n  if (digits.length === 2) {\n    return [[0], digits];\n  }\n  return [digits.slice(0, digits.length - 2), digits.slice(digits.length - 2, digits.length)];\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"strict\": true,\n    \"types\": []\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/classic-components/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/rewind/.babelrc",
    "content": "{\n  \"presets\": [[\"@nrwl/web/babel\", { \"useBuiltIns\": \"usage\" }]]\n}\n"
  },
  {
    "path": "libs/osu-pixi/rewind/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/rewind/README.md",
    "content": "# osu-pixi-rewind\n\nThis library was generated with [Nx](https://nx.dev).\n\n## Running unit tests\n\nRun `nx test osu-pixi-rewind` to execute the unit tests via [Jest](https://jestjs.io).\n"
  },
  {
    "path": "libs/osu-pixi/rewind/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-pixi-rewind\",\n  preset: \"../../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  transform: {\n    \"^.+\\\\.[tj]sx?$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"tsx\", \"js\", \"jsx\"],\n  coverageDirectory: \"../../../coverage/libs/osu-pixi/rewind\",\n};\n"
  },
  {
    "path": "libs/osu-pixi/rewind/src/index.ts",
    "content": "export * from \"./lib/AnalysisCursor\";\n"
  },
  {
    "path": "libs/osu-pixi/rewind/src/lib/AnalysisCursor.ts",
    "content": "import { Container } from \"@pixi/display\";\nimport { Graphics } from \"pixi.js\";\nimport { applyEasing, Easing, Position, Vec2 } from \"@osujs/math\";\n\nexport class AnalysisCross extends Graphics {\n  prepare(color: number, interesting?: boolean) {\n    this.clear();\n\n    // osu!px\n    const size = interesting ? 5 : 2;\n    const width = interesting ? 2 : 1;\n\n    this.lineStyle(width, color);\n    /* / */\n    this.moveTo(-size, size);\n    this.lineTo(size, -size);\n    /* \\ */\n    this.moveTo(-size, -size);\n    this.lineTo(+size, +size);\n  }\n}\n\nconst numberOfFrames = 25;\n\nexport enum AnalysisColorStyle {\n  RAW, // just display the color based on the keys that are pressed (similar to circle guard)\n  NEW, // only display the \"new\" key presses\n}\n\nexport interface AnalysisPoint {\n  position: Position;\n  color: number;\n  interesting: boolean;\n}\n\nconst colorScheme = [\n  0x5d6463, // none gray\n  0xffa500, // left (orange)\n  0x00ff00, // right (green)\n  // 0xfa0cd9, // right (pink)\n  0x3cbdc1, // both (cyan)\n];\n\ninterface Settings {\n  points: AnalysisPoint[];\n  smoothedPosition: Position;\n}\n\nconst defaultSettings: Settings = {\n  points: [],\n  smoothedPosition: { x: 0, y: 0 },\n};\n\nexport class AnalysisCursor {\n  container: Container;\n  analysisPoints: AnalysisCross[];\n  trail: Graphics;\n  circle: Graphics; // small circle at the smoothed position\n\n  constructor() {\n    this.container = new Container();\n    this.analysisPoints = [];\n    this.container.addChild((this.trail = new Graphics()));\n    for (let i = 0; i < numberOfFrames; i++) {\n      this.container.addChild((this.analysisPoints[numberOfFrames - i - 1] = new AnalysisCross()));\n    }\n    this.container.addChild((this.circle = new Graphics()));\n    this.circle.beginFill(0xffff00, 1.0);\n    this.circle.drawCircle(0, 0, 2);\n    this.circle.endFill();\n  }\n\n  prepare(settings: Partial<Settings>): void {\n    const { points, smoothedPosition } = { ...defaultSettings, ...settings };\n    // Trail\n    this.trail.clear();\n    this.trail.moveTo(0, 0);\n    const numberOfFrames = points.length;\n    for (let i = 0; i < numberOfFrames; i++) {\n      const { color, interesting, position } = points[i];\n      const f = (numberOfFrames - i) / numberOfFrames;\n      const baseAlpha = 1 - applyEasing(1 - f, Easing.OUT);\n      const offset = Vec2.sub(position, smoothedPosition);\n\n      this.trail.lineStyle(1, 0x5d6463, baseAlpha);\n      this.trail.lineTo(offset.x, offset.y);\n      this.analysisPoints[i].prepare(color, interesting);\n      this.analysisPoints[i].alpha = (interesting ? 1 : 0.6) * baseAlpha;\n      this.analysisPoints[i].position.set(offset.x, offset.y);\n    }\n  }\n}\n"
  },
  {
    "path": "libs/osu-pixi/rewind/tsconfig.json",
    "content": "{\n  \"extends\": \"../../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.lib.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/rewind/tsconfig.lib.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"declaration\": true,\n    \"types\": []\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "libs/osu-pixi/rewind/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.spec.tsx\",\n    \"**/*.test.tsx\",\n    \"**/*.spec.js\",\n    \"**/*.test.js\",\n    \"**/*.spec.jsx\",\n    \"**/*.test.jsx\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "migrations.json",
    "content": "{\n  \"migrations\": [\n    {\n      \"version\": \"13.10.0-beta.0\",\n      \"description\": \"Update the decorate-angular-cli script to require nx instead of @nrwl/cli\",\n      \"cli\": \"nx\",\n      \"implementation\": \"./src/migrations/update-13-10-0/update-decorate-cli\",\n      \"package\": \"@nrwl/workspace\",\n      \"name\": \"13-10-0-update-decorate-cli\"\n    },\n    {\n      \"version\": \"13.10.0-beta.0\",\n      \"description\": \"Update the tasks runner property to import it from the nx package instead of @nrwl/worksapce\",\n      \"cli\": \"nx\",\n      \"implementation\": \"./src/migrations/update-13-10-0/update-tasks-runner\",\n      \"package\": \"@nrwl/workspace\",\n      \"name\": \"13-10-0-update-tasks-runner\"\n    },\n    {\n      \"version\": \"14.0.0-beta.0\",\n      \"description\": \"Changes the presets in nx.json to come from the nx package\",\n      \"cli\": \"nx\",\n      \"implementation\": \"./src/migrations/update-14-0-0/change-nx-json-presets\",\n      \"package\": \"@nrwl/workspace\",\n      \"name\": \"14-0-0-change-nx-json-presets\"\n    },\n    {\n      \"version\": \"14.0.0-beta.0\",\n      \"description\": \"Migrates from @nrwl/workspace:run-script to nx:run-script\",\n      \"cli\": \"nx\",\n      \"implementation\": \"./src/migrations/update-14-0-0/change-npm-script-executor\",\n      \"package\": \"@nrwl/workspace\",\n      \"name\": \"14-0-0-change-npm-script-executor\"\n    },\n    {\n      \"version\": \"14.2.0\",\n      \"description\": \"Explicitly enable sourceAnalysis for all workspaces extending from npm.json or core.json (this was default behavior prior to 14.2)\",\n      \"cli\": \"nx\",\n      \"implementation\": \"./src/migrations/update-14-2-0/enable-source-analysis\",\n      \"package\": \"@nrwl/workspace\",\n      \"name\": \"14-2-0-enable-source-analysis\"\n    },\n    {\n      \"version\": \"14.0.0-beta.2\",\n      \"cli\": \"nx\",\n      \"description\": \"Update move jest config files to .ts files.\",\n      \"factory\": \"./src/migrations/update-14-0-0/update-jest-config-ext\",\n      \"package\": \"@nrwl/jest\",\n      \"name\": \"update-jest-config-extensions\"\n    },\n    {\n      \"version\": \"14.1.5-beta.0\",\n      \"cli\": \"nx\",\n      \"description\": \"Update to export default in jest config and revert jest.preset.ts to jest.preset.js\",\n      \"factory\": \"./src/migrations/update-14-1-5/update-exports-jest-config\",\n      \"package\": \"@nrwl/jest\",\n      \"name\": \"update-to-export-default\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.1.9-beta.0\",\n      \"description\": \"Adds @swc/core and @swc-node as a dev dep if you are using them\",\n      \"factory\": \"./src/migrations/update-14-1-9/add-swc-deps-if-needed\",\n      \"package\": \"@nrwl/linter\",\n      \"name\": \"add-swc-deps\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.2.3-beta.0\",\n      \"description\": \"Adds @swc/core and @swc-node as a dev dep if you are using them (repeated due to prior mistake)\",\n      \"factory\": \"./src/migrations/update-14-1-9/add-swc-deps-if-needed\",\n      \"package\": \"@nrwl/linter\",\n      \"name\": \"add-swc-deps-again\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.4.4\",\n      \"description\": \"Adds @typescript-eslint/utils as a dev dep\",\n      \"factory\": \"./src/migrations/update-14-4-4/experimental-to-utils-deps\",\n      \"package\": \"@nrwl/linter\",\n      \"name\": \"experimental-to-utils-deps\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.4.4\",\n      \"description\": \"Switch from  @typescript-eslint/experimental-utils to @typescript-eslint/utils in all rules and rules.spec files\",\n      \"factory\": \"./src/migrations/update-14-4-4/experimental-to-utils-rules\",\n      \"package\": \"@nrwl/linter\",\n      \"name\": \"experimental-to-utils-rules\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"13.10.0-beta.0\",\n      \"description\": \"Update to React 18\",\n      \"factory\": \"./src/migrations/update-13-10-0/update-13-10-0\",\n      \"package\": \"@nrwl/react\",\n      \"name\": \"update-react-18-13.10.0\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.0.0-beta.0\",\n      \"description\": \"Update to React DOM render call to React 18 API.\",\n      \"factory\": \"./src/migrations/update-14-0-0/update-react-dom-render-for-v18\",\n      \"package\": \"@nrwl/react\",\n      \"name\": \"update-react-dom-render-14.0.0\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.0.0-beta.0\",\n      \"description\": \"Replace deprecated '@testing-library/react-hook' package with `renderHook` from '@testing-library/react'.\",\n      \"factory\": \"./src/migrations/update-14-0-0/replace-testing-library-react-hook\",\n      \"package\": \"@nrwl/react\",\n      \"name\": \"replace-testing-library-react-hook-14.0.0\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.0.0-beta.0\",\n      \"description\": \"Add a default development configuration for build and serve targets.\",\n      \"factory\": \"./src/migrations/update-14-0-0/add-default-development-configurations\",\n      \"package\": \"@nrwl/react\",\n      \"name\": \"add-default-development-configurations-14.0.0\"\n    },\n    {\n      \"cli\": \"nx\",\n      \"version\": \"14.1.0-beta.0\",\n      \"description\": \"Update external option in projects for Emotion\",\n      \"factory\": \"./src/migrations/update-14-1-0/update-external-emotion-jsx-runtime\",\n      \"package\": \"@nrwl/react\",\n      \"name\": \"update-external-emotion-jsx-runtime-14.1.0\"\n    },\n    {\n      \"version\": \"14.0.0\",\n      \"cli\": \"nx\",\n      \"description\": \"Migrate Storybook to v6\",\n      \"factory\": \"./src/migrations/update-14-0-0/migrate-to-storybook-6\",\n      \"package\": \"@nrwl/storybook\",\n      \"name\": \"update-14.0.0\"\n    },\n    {\n      \"version\": \"14.1.8\",\n      \"cli\": \"nx\",\n      \"description\": \"Change storybook targets for Angular projects to use @storybook/angular executors\",\n      \"factory\": \"./src/migrations/update-14-1-8/change-storybook-targets\",\n      \"package\": \"@nrwl/storybook\",\n      \"name\": \"update-14.1.8\"\n    }\n  ]\n}\n"
  },
  {
    "path": "nx.json",
    "content": "{\n  \"npmScope\": \"rewind\",\n  \"affected\": {\n    \"defaultBase\": \"master\"\n  },\n  \"implicitDependencies\": {\n    \"package.json\": {\n      \"dependencies\": \"*\",\n      \"devDependencies\": \"*\"\n    },\n    \".eslintrc.json\": \"*\"\n  },\n  \"tasksRunnerOptions\": {\n    \"default\": {\n      \"runner\": \"nx/tasks-runners/default\",\n      \"options\": {\n        \"cacheableOperations\": [\n          \"build\",\n          \"lint\",\n          \"test\",\n          \"e2e\",\n          \"build-storybook\"\n        ],\n        \"parallel\": 1\n      }\n    }\n  },\n  \"targetDependencies\": {\n    \"build\": [\n      {\n        \"target\": \"build\",\n        \"projects\": \"dependencies\"\n      }\n    ]\n  },\n  \"cli\": {\n    \"defaultCollection\": \"@nrwl/react\"\n  },\n  \"generators\": {\n    \"@nrwl/react\": {\n      \"application\": {\n        \"style\": \"css\",\n        \"linter\": \"eslint\",\n        \"babel\": true\n      },\n      \"component\": {\n        \"style\": \"css\"\n      },\n      \"library\": {\n        \"style\": \"css\",\n        \"linter\": \"eslint\"\n      }\n    },\n    \"@nrwl/web:application\": {\n      \"style\": \"css\",\n      \"linter\": \"eslint\",\n      \"unitTestRunner\": \"jest\",\n      \"e2eTestRunner\": \"none\"\n    },\n    \"@nrwl/web:library\": {\n      \"style\": \"css\",\n      \"linter\": \"eslint\",\n      \"unitTestRunner\": \"jest\"\n    }\n  }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"rewind\",\n  \"version\": \"0.2.2\",\n  \"private\": true,\n  \"license\": \"MIT\",\n  \"author\": \"abstrakt\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"affected\": \"nx affected\",\n    \"affected:apps\": \"nx affected:apps\",\n    \"affected:build\": \"nx affected:build\",\n    \"affected:dep-graph\": \"nx affected:dep-graph\",\n    \"affected:e2e\": \"nx affected:e2e\",\n    \"affected:libs\": \"nx affected:libs\",\n    \"affected:lint\": \"nx affected:lint\",\n    \"affected:test\": \"nx affected:test\",\n    \"build\": \"yarn run desktop:build:prod\",\n    \"build-storybook\": \"build-storybook\",\n    \"dep-graph\": \"nx dep-graph\",\n    \"desktop:build:prod\": \"nx run-many --skip-nx-cache --target=build --projects=desktop-frontend,desktop-main --prod && electron-builder\",\n    \"desktop-frontend:dev\": \"nx run desktop-frontend:serve\",\n    \"desktop-main:serve\": \"electron dist/apps/desktop/index.js\",\n    \"desktop-main:dev\": \"nx run desktop-main:build && yarn run desktop-main:serve\",\n    \"e2e\": \"nx e2e\",\n    \"format\": \"nx format:write\",\n    \"format:check\": \"nx format:check\",\n    \"format:write\": \"nx format:write\",\n    \"help\": \"nx help\",\n    \"lint\": \"nx workspace-lint && nx lint\",\n    \"nx\": \"nx\",\n    \"osujs:local:publish\": \"cd dist/libs/osu && yalc push core && yalc push math && yalc push pp\",\n    \"storybook\": \"start-storybook -p 6006\",\n    \"test\": \"nx test\",\n    \"update\": \"nx migrate latest\",\n    \"workspace-generator\": \"nx workspace-generator\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\"\n    }\n  },\n  \"lint-staged\": {\n    \"*.{js,json,css,scss,md,ts,html,graphql}\": [\n      \"yarn format --uncommitted\"\n    ]\n  },\n  \"dependencies\": {\n    \"ajv\": \"^8.6.3\",\n    \"chart.js\": \"^3.5.1\",\n    \"chokidar\": \"^3.5.2\",\n    \"color-string\": \"^1.6.0\",\n    \"core-js\": \"^3.17.2\",\n    \"electron-log\": \"^4.4.1\",\n    \"electron-store\": \"^8.0.1\",\n    \"electron-updater\": \"^4.6.1\",\n    \"history\": \"^5.2.0\",\n    \"immer\": \"^9.0.6\",\n    \"inversify\": \"^6.2.1\",\n    \"node-osr\": \"^1.2.1\",\n    \"reflect-metadata\": \"^0.1.13\",\n    \"regenerator-runtime\": \"0.13.9\",\n    \"rxjs\": \"^7.3.0\",\n    \"rxjs-hooks\": \"^0.7.0\",\n    \"semver\": \"^7.3.5\",\n    \"simple-statistics\": \"^7.7.0\",\n    \"stats.js\": \"^0.17.0\",\n    \"tslib\": \"^2.0.0\",\n    \"typescript-collections\": \"^1.3.3\",\n    \"username\": \"5.1.0\",\n    \"utility-types\": \"^3.10.0\",\n    \"walk\": \"^2.3.15\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"7.15.4\",\n    \"@babel/preset-env\": \"7.15.4\",\n    \"@babel/preset-react\": \"7.14.5\",\n    \"@babel/preset-typescript\": \"7.15.0\",\n    \"@emotion/react\": \"11.9.3\",\n    \"@emotion/styled\": \"11.9.3\",\n    \"@heroicons/react\": \"^1.0.4\",\n    \"@mui/icons-material\": \"^5.0.1\",\n    \"@mui/material\": \"^5.0.2\",\n    \"@nestjs/common\": \"9.0.0\",\n    \"@nestjs/core\": \"9.0.0\",\n    \"@nestjs/event-emitter\": \"^1.0.0\",\n    \"@nestjs/platform-express\": \"9.0.0\",\n    \"@nestjs/platform-socket.io\": \"^8.0.6\",\n    \"@nestjs/schematics\": \"9.0.0\",\n    \"@nestjs/testing\": \"9.0.0\",\n    \"@nestjs/websockets\": \"^8.0.6\",\n    \"@nrwl/cli\": \"14.5.1\",\n    \"@nrwl/cypress\": \"14.5.1\",\n    \"@nrwl/eslint-plugin-nx\": \"14.5.1\",\n    \"@nrwl/jest\": \"14.5.1\",\n    \"@nrwl/linter\": \"14.5.1\",\n    \"@nrwl/nest\": \"14.5.1\",\n    \"@nrwl/node\": \"14.5.1\",\n    \"@nrwl/react\": \"14.5.1\",\n    \"@nrwl/storybook\": \"14.5.1\",\n    \"@nrwl/web\": \"14.5.1\",\n    \"@nrwl/workspace\": \"14.5.1\",\n    \"@pixi/filter-adjustment\": \"^4.1.3\",\n    \"@reduxjs/toolkit\": \"1.8.3\",\n    \"@storybook/addon-actions\": \"^6.3.8\",\n    \"@storybook/addon-essentials\": \"6.5.9\",\n    \"@storybook/addon-links\": \"^6.3.8\",\n    \"@storybook/builder-webpack5\": \"6.5.9\",\n    \"@storybook/core-server\": \"6.4.22\",\n    \"@storybook/manager-webpack5\": \"6.5.9\",\n    \"@storybook/react\": \"6.5.9\",\n    \"@svgr/webpack\": \"^5.4.0\",\n    \"@testing-library/react\": \"13.3.0\",\n    \"@types/add-zero\": \"^1.0.1\",\n    \"@types/color-string\": \"^1.5.0\",\n    \"@types/decimal.js\": \"^7.4.0\",\n    \"@types/jest\": \"27.4.1\",\n    \"@types/node\": \"18.0.4\",\n    \"@types/node-osr\": \"file:./libs/@types/node-osr\",\n    \"@types/react\": \"^19.0.8\",\n    \"@types/react-dom\": \"^19.0.3\",\n    \"@types/react-page-visibility\": \"^6.4.1\",\n    \"@types/semver\": \"^7.3.9\",\n    \"@types/stats.js\": \"^0.17.0\",\n    \"@types/styled-components\": \"5.1.25\",\n    \"@types/supertest\": \"^2.0.11\",\n    \"@types/walk\": \"^2.3.1\",\n    \"@types/ws\": \"^8.2.0\",\n    \"@typescript-eslint/eslint-plugin\": \"5.29.0\",\n    \"@typescript-eslint/parser\": \"5.29.0\",\n    \"adm-zip\": \"^0.5.6\",\n    \"babel-jest\": \"27.5.1\",\n    \"babel-loader\": \"8.2.2\",\n    \"concurrently\": \"^7.1.0\",\n    \"copy-webpack-plugin\": \"^10.2.4\",\n    \"cypress\": \"^8.3.1\",\n    \"dotenv\": \"^16.0.0\",\n    \"electron\": \"^19.0.3\",\n    \"electron-builder\": \"^23.3.3\",\n    \"eslint\": \"8.15.0\",\n    \"eslint-config-prettier\": \"8.3.0\",\n    \"eslint-plugin-cypress\": \"^2.10.3\",\n    \"eslint-plugin-import\": \"2.26.0\",\n    \"eslint-plugin-jsx-a11y\": \"6.6.1\",\n    \"eslint-plugin-react\": \"7.30.1\",\n    \"eslint-plugin-react-hooks\": \"4.6.0\",\n    \"exitzero\": \"^1.0.1\",\n    \"generate-package-json-webpack-plugin\": \"^2.6.0\",\n    \"husky\": \"^7.0.2\",\n    \"jest\": \"27.5.1\",\n    \"jest-match-object-close-to\": \"^1.0.2\",\n    \"json-log-viewer\": \"^0.1.2\",\n    \"lint-staged\": \"^11.1.2\",\n    \"nest-winston\": \"^1.6.0\",\n    \"ojsama\": \"^2.2.0\",\n    \"pixi.js\": \"^6.1.2\",\n    \"prettier\": \"2.6.2\",\n    \"react\": \"^19.0.0\",\n    \"react-dom\": \"^19.0.0\",\n    \"react-hotkeys-hook\": \"^3.4.0\",\n    \"react-icons\": \"^4.2.0\",\n    \"react-page-visibility\": \"^6.4.0\",\n    \"react-redux\": \"8.0.2\",\n    \"react-router-dom\": \"^7.1.3\",\n    \"react-spinners\": \"^0.11.0\",\n    \"react-test-renderer\": \"18.0.0\",\n    \"redux-saga\": \"^1.1.3\",\n    \"socket.io\": \"^4.2.0\",\n    \"socket.io-client\": \"^4.2.0\",\n    \"storybook-css-modules-preset\": \"^1.1.1\",\n    \"styled-components\": \"^6.1.14\",\n    \"superagent\": \"^6.1.0\",\n    \"supertest\": \"^6.1.6\",\n    \"ts-jest\": \"27.1.4\",\n    \"ts-node\": \"10.8.2\",\n    \"tsconfig-paths\": \"^3.12.0\",\n    \"typescript\": \"^5.7.3\",\n    \"url-loader\": \"^4.1.1\",\n    \"winston\": \"^3.3.3\"\n  }\n}\n"
  },
  {
    "path": "resources/Skins/OsuDefaultSkin/README.md",
    "content": "This is the osu!legacy skin in https://github.com/ppy/osu-resources.\n\nThis skin is used in a non-commercial manner. Please contact me if you are the owner of the skin, and it should be removed from Rewind.\n"
  },
  {
    "path": "resources/Skins/RewindDefaultSkin/README.md",
    "content": "This is the -YUGEN- skin: https://osuskins.net/skin/wEaMJGb\n\nNot stored in the .git repository, instead just download and unzip it here.\n\nThis skin is used in a non-commercial manner. Please contact me if you are the owner of the skin, and it should be removed from Rewind.\n"
  },
  {
    "path": "resources/Skins/RewindDefaultSkin/skin.ini",
    "content": "[General]\nName: - YUGEN FINAL - Widescreen\nAuthor: [Garin]\nVersion: 2.4\n\nSliderBallFlip: 1\nCursorRotate: 0\nCursorTrailRotate: 0\nCursorExpand: 0\nCursorCentre: 1\nSliderBallFrames: 60\nHitCircleOverlayAboveNumer: 1\nSliderStyle: 2\n\nAllowSliderBallTint: 1\n\n\nSpinnerFadePlayfield: 0\n[Colours]\nCombo1: 26,116,242\nCombo2: 164,32,240\nCombo3: 37,185,239\nCombo4: 23,209,116\nCombo5: 226,45,124\n\nSongSelectActiveText: 250,250,250\nSongSelectInactiveText: 230,230,230\n\n\nSliderBorder: 190,190,190\nSliderTrackOverride: 3,3,12\n\n[Fonts]\nHitCirclePrefix: default\nHitCircleOverlap: 6\n\nScorePrefix: score\nScoreOverlap: 8\n\nComboPrefix: combo\nComboOverlap: 8\n\n[Mania]\nKeys: 4\n//Mania skin config\nColumnStart: 340\nHitPosition: 400\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 0\nScorePosition: 300\nComboPosition: 275\nLightFramePerSecond: 24\nColumnWidth: 45,45,45,45\nColumnLineWidth: 0,0,0,0,0\nBarlineHeight: 0\n//Colours\nColourLight1: 102,205,107,175\nColourLight2: 69,188,250,175\nColourLight3: 69,188,250,175\nColourLight4: 102,205,107,175\nColour1: 0,0,0,240\nColour2: 0,0,0,240\nColour3: 0,0,0,240\nColour4: 0,0,0,240\nColourHold: 255,230,0,255\n\n\n[Mania]\nKeys: 5\n//Mania skin config\nColumnStart: 336\nHitPosition: 400\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 0\nScorePosition: 300\nComboPosition: 275\nLightFramePerSecond: 24\nColumnWidth: 43,40,44,40,43\nColumnLineWidth: 0,0,0,0,0,0\nBarlineHeight: 0\n//Colours\nColourLight1: 102,205,107,175\nColourLight2: 69,188,250,175\nColourLight3: 205,102,102,175\nColourLight4: 69,188,250,175\nColourLight5: 102,205,107,175\nColour1: 0,0,0,240\nColour2: 0,0,0,240\nColour3: 0,0,0,240\nColour4: 0,0,0,240\nColour5: 0,0,0,240\nColourHold: 255,230,0,255\n//images\nKeyImage2: mania-keyS\nKeyImage2D: mania-keySD\nNoteImage2: mania-noteS\nNoteImage2H: mania-noteSH\nNoteImage2L: mania-noteSL\n\n\n[Mania]\nKeys: 6\n//Mania skin config\nColumnStart: 336\nHitPosition: 400\nSpecialStyle: 0\nUpsideDown: 0\nJudgementLine: 0\nScorePosition: 300\nComboPosition: 275\nLightFramePerSecond: 24\nColumnWidth: 38,35,38,35,38,35\nColumnLineWidth: 0,0,0,0,0,0,0\nBarlineHeight: 0\n//Colours\nColourLight1: 102,205,107,175\nColourLight2: 69,188,250,175\nColourLight3: 102,205,107,175\nColourLight4: 102,205,107,175\nColourLight5: 69,188,250,175\nColourLight6: 102,205,107,175\nColour1: 0,0,0,240\nColour2: 0,0,0,240\nColour3: 0,0,0,240\nColour4: 0,0,0,240\nColour5: 0,0,0,240\nColour6: 0,0,0,240\nColourHold: 255,230,0,255\n\n\n[Mania]\nKeys: 7\n//Mania skin config\nColumnStart: 336\nHitPosition: 400\nScorePosition: 300\nComboPosition: 275\nJudgementLine: 0\nLightFramePerSecond: 24\nColumnWidth: 36,34,36,38,36,34,38\nColumnLineWidth: 0,0,0,0,0,0,0,0\nBarlineHeight: 0\n//Colours\nColourLight1: 102,205,170,255\nColourLight2: 69,188,250,175\nColourLight3: 102,205,170,255\nColourLight4: 205,102,102,175\nColourLight5: 102,205,170,255\nColourLight6: 69,188,250,175\nColourLight7: 102,205,170,255\nColour1: 0,0,0,240\nColour2: 0,0,0,240\nColour3: 0,0,0,240\nColour4: 0,0,0,240\nColour5: 0,0,0,240\nColour6: 0,0,0,240\nColour7: 0,0,0,240\nColourHold: 255,230,0,255\n\n\n[Mania]\nKeys: 8\n//Mania skin config\nColumnStart: 336\nBarline: 0\nHitPosition: 400\nSpecialStyle: 1\nUpsideDown: 0\nJudgementLine: 0\nScorePosition: 300\nComboPosition: 275\nLightFramePerSecond: 24\nColumnWidth: 45,28,25,28,25,28,25,28\nColumnLineWidth: 0,2,2,2,2,2,2,2,0\nBarlineHeight: 0\n//Colours\nColour1: 0,0,0,240\nColour2: 24,24,24,240\nColour3: 0,0,0,240\nColour4: 24,24,24,240\nColour5: 0,0,0,240\nColour6: 24,24,24,240\nColour7: 0,0,0,240\nColour8: 24,24,24,240\nColourLight1: 205,102,102,175\nColourLight2: 102,205,107,175\nColourLight3: 69,188,250,175\nColourLight4: 102,205,107,175\nColourLight5: 69,188,250,175\nColourLight6: 102,205,107,175\nColourLight7: 69,188,250,175\nColourLight8: 102,205,107,175\nColourHold: 255,255,255,255\nColourColumnLine: 99,99,99,255\nColourHold: 255,230,0,255\n//images\nKeyImage0: Mania-keyT\nKeyImage0D: Mania-KeyTD\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Short kick slider] (2021-07-16) Perfect.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a0d29d547f02fd5005135421b733df33064ae3d88e4868098c15ad3c4d73aa9d\nsize 751\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Short kick slider] (2021-07-16) TooLateMissed.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:49a1f9a90161969af0a6c8eb3908b9090e248ba9b9c9632adbfc7198b8397940\nsize 780\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider (Repeat = 1)] (2021-07-07) Perfect.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f9bccdfb47e59662231ee4473739f6e028a5a5f3f7ed8c79caea8fd480976a62\nsize 850\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) Osu Perfect.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cf0cdc3cf012887d92b70656b0446b081403fd794bcb327e56d4865e6eda4a48\nsize 1005\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) Osu SliderHeadMissedButTrackingWtf.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a1c42a2b10da5292213ba62b331775e5ee14df5cdc1cec0469896288c9ce38c2\nsize 984\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) Osu SliderHeadTooEarly.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6473d993815cf5a6636f83ce6e851e79facc2378530588ad2e50a37760a2c81f\nsize 859\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) Osu SliderHeadTooLate.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8436aeb870a58e41f57620ad2c6f68bcdeac3c74c97dc11141fe1046c68d12c9\nsize 942\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) Perfect.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cf0cdc3cf012887d92b70656b0446b081403fd794bcb327e56d4865e6eda4a48\nsize 1005\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderEndMissed.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:61b4f13f848a21ce3bc0a60cbd5a14000e3046a3348d362ff7907f0adb9b7e24\nsize 1058\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderHeadMissedButTrackingWtf.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a1c42a2b10da5292213ba62b331775e5ee14df5cdc1cec0469896288c9ce38c2\nsize 984\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderHeadTooEarly.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6473d993815cf5a6636f83ce6e851e79facc2378530588ad2e50a37760a2c81f\nsize 859\n"
  },
  {
    "path": "testdata/osu!/Replays/- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderHeadTooLate.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8436aeb870a58e41f57620ad2c6f68bcdeac3c74c97dc11141fe1046c68d12c9\nsize 942\n"
  },
  {
    "path": "testdata/osu!/Replays/RyuK - HoneyWorks - Akatsuki Zukuyo [Taeyang_s Extra] (2019-06-08) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6e3ab2a2cd6a7b52139190eeeb686067a9e497a11199b801d29869451999d98f\nsize 94584\n"
  },
  {
    "path": "testdata/osu!/Replays/Varvalian - Aether Realm - The Sun, The Moon, The Star [Mourning Those Things I've Long Left Behind] (2019-05-15) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f8a62450e9918e7906da202eea880bb72f0040f3dfb3ba7f9c938d50d9319614\nsize 343517\n"
  },
  {
    "path": "testdata/osu!/Replays/abstrakt - Gojou Mayumi - DANZEN! Futari wa PreCure Ver. MaxHeart (TV Size) [Insane] (2021-08-21) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f0678bd5efd052316d88c014dfddd09ffa7e60df2fe650f5213c6899a9f5d8bd\nsize 20131\n"
  },
  {
    "path": "testdata/osu!/Replays/abstrakt - PSYQUI - Hype feat. Such [Dreamer] (2021-08-08) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:60e0e8fe7aa9da6dd555de9b95888f39ae7a45f395fbce2a93c4807f8e4438b6\nsize 99723\n"
  },
  {
    "path": "testdata/osu!/Replays/abstrakt - SHK - Violet Perfume [Insane] (2021-03-27) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:61adef2d59a199d2a0c237417ff58b29b67605628a2a91ebde28bbf726688d4b\nsize 34448\n"
  },
  {
    "path": "testdata/osu!/Replays/abstrakt - Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) [Couch Mini2a] (2021-04-09) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:546c72f5f72d38d611067c0ee8dd1b4e96e5725decdb58908cbbec571ff5380b\nsize 69510\n"
  },
  {
    "path": "testdata/osu!/Replays/abstrakt - sabi - true DJ MAG top ranker_s song Zenpen (katagiri Remix) [Senseabel's Extra] (2021-08-08) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6670345d5729e41e097f74a49ce2e6ef6515c19b01b101d5dab4c7881a361ab3\nsize 99333\n"
  },
  {
    "path": "testdata/osu!/Replays/hallowatcher - DECO27 - HIBANA feat. Hatsune Miku [Lock On] (2020-02-09) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:290c67e185f85fc11f8b30dfd8b83913e39e9647261247d7ed4035dc5c4b4adc\nsize 66558\n"
  },
  {
    "path": "testdata/osu!/Replays/kellad - Asriel - Raison D'etre [EXist] (2025-02-16) Osu.osr",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b7fedbd2b45762cafbb57dcaf70d0a4654a73f76b67f21397356616e85b44b51\nsize 120235\n"
  },
  {
    "path": "testdata/osu!/Songs/1001507 ZUTOMAYO - Kan Saete Kuyashiiwa/ZUTOMAYO - Kan Saete Kuyashiiwa (Nathan) [geragera].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:565f42aea8bf0f3dba12e936b4188d5f551a740842f9e3bd8c3ee71575b731b2\nsize 76577\n"
  },
  {
    "path": "testdata/osu!/Songs/1010865 SHK - Violet Perfume [no video]/SHK - Violet Perfume (ktgster) [Insane].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:833910b2028a47bdf335e4d35da282560b2c791db219ed9d3853e0c70ec940a5\nsize 17423\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [.-- .-. --- -. --. .-- .- -.--].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3f44e656f43b0ffb6c4c065782e7f1bd23ed01581c4c85dea6107c0ce012809d\nsize 14591180\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Beloved Exclusive].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0648416ce18df36e5c2d4bfd309f41cfa7bf8eb08adf368a426d99052ccb5a42\nsize 12879703\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Earth (atm)].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f043935ae95932e998876794aaa34f874b7b8005c848b4cf5215b1d94b64d3e4\nsize 2335935\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Earth].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2e9d0d419e9a57b4e07200f315695396091e6e82224e5cbe2d09380453b8b08b\nsize 3155775\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Fire].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:17475bb54126f6e421f2ec197cb294e4a714dd5a30ed1b8748cc0322d306a716\nsize 329775\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Metal].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:292b69cd5f5573a729d15ca6d1b4490f7ba249f5f84954291d299b777ba99888\nsize 132755\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Moon].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:9046fd946dbae469d11a4d44c71d8f96c842aeee276d30bfa9168d15b11d616c\nsize 7141750\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Sun].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:43a1d1c8b5515592c32cb59574a38817f6f67083339d96776bd2059f8158eafa\nsize 731090\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Water].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d8a00a94aeb183350f884929cc77143a397bf60d70af379f7f3b4e74a3a7f25c\nsize 1207205\n"
  },
  {
    "path": "testdata/osu!/Songs/1236927 Frums - XNOR XNOR XNOR/Frums - XNOR XNOR XNOR (fanzhen0019) [Wood].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:51c5d4878648f26ab99861d2eac5f408e1187ab1cde641dfb62ac17dcb2766df\nsize 1014361\n"
  },
  {
    "path": "testdata/osu!/Songs/1302792 Smiledk - Koko Soko (AKIBA KOUBOU Eurobeat Remix)/Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) ([ Couch ] Mini) [Couch Mini2a].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f957d59d4f62209e83ed63468cf45734e1cdf54d9fce9ca4b4437f0e96a8271e\nsize 49369\n"
  },
  {
    "path": "testdata/osu!/Songs/1357624 sabi - true DJ MAG top ranker's song Zenpen (katagiri Remix)/sabi - true DJ MAG top ranker's song Zenpen (katagiri Remix) (Nathan) [KEMOMIMI EDM SQUAD].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2abeb81c2daa17259e5f2674161599c85977adf61f8de232832874a83bab3430\nsize 209513\n"
  },
  {
    "path": "testdata/osu!/Songs/1495211 Aether Realm - The Tower/Aether Realm - The Tower (Takane) [Brick and Mortar].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8fe1d619c0d9e795ba5fc401a6465fc580b6f784352db86bf71db762895c779e\nsize 36839\n"
  },
  {
    "path": "testdata/osu!/Songs/150945 Knife Party - Centipede/Knife Party - Centipede (Sugoi-_-Desu) [This isn't a map, just a simple visualisation].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e7e4e7d598907f0f6edaf40797f8dfafdb0602642aa4511ee3eeef3110319b94\nsize 179712\n"
  },
  {
    "path": "testdata/osu!/Songs/158023 UNDEAD CORPORATION - Everything will freeze/UNDEAD CORPORATION - Everything will freeze (Ekoro) [Time Freeze].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:29b8c8c0a413e237788013fc8c460f9601d1f61d41ee17c21035527cf7cdc43f\nsize 58157\n"
  },
  {
    "path": "testdata/osu!/Songs/29157 Within Temptation - The Unforgiving [no video]/Within Temptation - The Unforgiving (Armin) [Marathon].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:48f1650f1e0eb6a981f137042a8771037b19a757ae90a13600ce56268abea9c0\nsize 243197\n"
  },
  {
    "path": "testdata/osu!/Songs/351280 HoneyWorks - Akatsuki Zukuyo/HoneyWorks - Akatsuki Zukuyo ([C u r i]) [Taeyang's Extra].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f46988357d7f4363dd32b8fb26592a33a859f60465698d8b1e9a531718a61cda\nsize 51567\n"
  },
  {
    "path": "testdata/osu!/Songs/607089 Xi - Rokujuu Nenme no Shinsoku Saiban _ Rapidity is a justice/Xi - Rokujuu Nenme no Shinsoku Saiban ~ Rapidity is a justice (tokiko) [Extra Stage].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c9b2c349e0feebbd6239d2642ca50047a40531910108a38c6099f2000367c6f1\nsize 88011\n"
  },
  {
    "path": "testdata/osu!/Songs/863227 Brian The Sun - Lonely Go! (TV Size) [no video]/Brian The Sun - Lonely Go! (TV Size) (Nevo) [Fiery's Extreme].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cb7b2a53692322b62a4dcf65d5996b0c050561ef497235cc7aec706c6eeaa7b5\nsize 20840\n"
  },
  {
    "path": "testdata/osu!/Songs/931596 Apol - Hidamari no Uta/Apol - Hidamari no Uta (-Keitaro) [Expert].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0d8a760ba6a1e6cbc307ab07b36546b390c7f7fffcb301d75ffcfc32cf7abc42\nsize 52842\n"
  },
  {
    "path": "testdata/osu!/Songs/933630 Aether Realm - The Sun, The Moon, The Star/Aether Realm - The Sun, The Moon, The Star (ItsWinter) [Mourning Those Things I've Long Left Behind].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f1700e63899e118736162380a9c640cff2fdd7912e6901fbf286e21f74d6c88f\nsize 294916\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Easy].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:9e0baf9c27e2caff8c2038ca434fc40025f6f7b787ea754032b00d0446d1227f\nsize 14349\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Hard].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d220987d22cbdfa79c6afd512fdc966de08ab71a93c9660d3c6ebb03488aeacc\nsize 28667\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [HitCircle 1].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d1dc817df864a65f4fd9aef5518e3cecb951f3648e5f5f71d64dc0fa9721cdbe\nsize 1745\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Normal].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:7625b50ed65e5661bde1962795b3a6b4ea2d76794f950885553f2f4f1a3f4860\nsize 20109\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Short kick slider].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:7b500f2293bc7f54a772ec34c1d6f1215f77526e546c3cbfe38806893409705b\nsize 1267\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Slider (Repeat = 1)].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c3b4affa84a5880a40b0cef4fe0f90282e8238490e198224ab014a0ad55f2a8c\nsize 1776\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Slider 1].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:66a686deafbedbcb4739546ecb81f04f1862a204f44441169aa441af933d1a53\nsize 1775\n"
  },
  {
    "path": "testdata/osu!/Songs/967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Smile].osu",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b0ae0244e503b1a7c165152c08ae6d25390a8bc6147f184fe34efdfe39df6629\nsize 35958\n"
  },
  {
    "path": "testdata/osu!/osu!.me.cfg",
    "content": "# osu! configuration for me\n# last updated on Wednesday, November 3, 2021\n\n# IMPORTANT: DO NOT SHARE THIS FILE PUBLICLY.\n# IT CONTAINS YOUR LOGIN CREDENTIALS IF YOU HAVE THEM SAVED.\n\nBeatmapDirectory = Songs\nVolumeUniversal = 40\nVolumeEffect = 100\nVolumeMusic = 30\nAllowPublicInvites = 1\nAutoChatHide = 1\nAutomaticDownload = 1\nAutomaticDownloadNoVideo = 0\nBlockNonFriendPM = 0\nShaders = 0\nBloomSoftening = 0\nBossKeyFirstActivation = 1\nChatAudibleHighlight = 1\nChatChannels = #osu #userlog #lobby\nChatFilter = 0\nChatHighlightName = 1\nChatMessageNotification = 1\nChatLastChannel = #lobby\nChatRemoveForeign = 0\nChatSortMode = Rank\nComboBurst = 0\nComboFire = 0\nComboFireHeight = 3\nConfirmExit = 0\nAutoSendNowPlaying = 1\nCursorSize = 0.82\nAutomaticCursorSizing = 0\nDimLevel = 100\nIHateHavingFun = 1\nDisplay = 2\nDisplayCityLocation = 0\nDiscordRichPresence = 0\nDistanceSpacingEnabled = 0\nEditorTip = 29\nEditorVideo = 0\nEditorDim = 0\nEditorGrid = 0\nEditorDefaultSkin = 1\nEditorSnakingSliders = 1\nEditorHitAnimations = 0\nEditorFollowPoints = 1\nEditorStacking = 1\nForceSliderRendering = 0\nFpsCounter = 1\nGuideTips =\nCursorRipple = 0\nHighlightWords =\nHighResolution = 0\nHitLighting = 0\nIgnoreBarline = 0\nIgnoreBeatmapSamples = 1\nIgnoreBeatmapSkins = 1\nIgnoreList =\nKeyOverlay = 1\nLanguage = en\nLastPlayMode = Osu\nAllowNowPlayingHighlights = 0\nLastVersion = b20211103.2\nLastVersionPermissionsFailed = b20210520.2\nLoadSubmittedThread = 1\nLobbyPlayMode = -1\nShowInterface = 1\nShowInterfaceDuringRelax = 0\nLobbyShowExistingOnly = 0\nLobbyShowFriendsOnly = 0\nLobbyShowFull = 0\nLobbyShowInProgress = 0\nLobbyShowPassworded = 0\nLogPrivateMessages = 0\nLowResolution = 0\nManiaSpeed = 12\nUsePerBeatmapManiaSpeed = 0\nManiaSpeedBPMScale = 0\nMenuTip = 26\nMouseDisableButtons = 1\nMouseDisableWheel = 0\nMouseSpeed = 1\nOffset = 0\nScoreMeterScale = 2.14\nDistanceSpacing = 0.3\nEditorBeatDivisor = 4\nEditorGridSize = 8\nEditorGridSizeDesign = 32\nHeight = 1080\nWidth = 1920\nHeightFullscreen = 1080\nCustomFrameLimit = 240\nWidthFullscreen = 1920\nMsnIntegration = 0\nMyPcSucks = 0\nNotifyFriends = 1\nNotifySubmittedThread = 1\nPopupDuringGameplay = 0\nProgressBarType = Pie\nRankType = Local\nRefreshRate = 60\nOverrideRefreshRate = 0\nScaleMode = WidescreenConservative\nScoreboardVisible = 0\nScoreMeter = Error\nScreenshotId = 158\nMenuSnow = 0\nMenuTriangles = 1\nSongSelectThumbnails = 1\nScreenshotFormat = Jpg\nShowReplayComments = 0\nShowSpectators = 1\nShowStoryboard = 0\nSkin = - Amaestric [1.1]\nSkinSamples = 1\nSkipTablet = 0\nSnakingSliders = 1\nTablet = 0\nUpdatePending = 0\nUserFilter = Friends\nUseSkinCursor = 1\nSeasonalBackgrounds = Never\nUseTaikoSkin = 0\nVideo = 0\nWiimote = 0\nYahooIntegration = 0\nForceFrameFlush = 0\nDetectPerformanceIssues = 1\nFullscreen = 0\nMenuMusic = 1\nMenuVoice = 1\nMenuParallax = 1\nRawInput = 0\nAbsoluteToOsuWindow = 0\nConfineMouse = Fullscreen\nShowMenuTips = 1\nHiddenShowFirstApproach = 1\nComboColourSliderBall = 0\nAlternativeChatFont = 0\nCredentialEndpoint =\nUsername =\nDisplayStarsMaximum = 10\nDisplayStarsMinimum = 0\nAudioDevice =\nAudioCompatibility = 0\nSavePassword = 1\nSaveUsername = 1\nPassword =\nTreeSortMode = Show_All\nTreeSortMode2 = Difficulty\nLetterboxing = 0\nLetterboxPositionX = 0\nLetterboxPositionY = 0\nFrameSync = PowerSaving\nShowUnicode = 0\nPermanentSongInfo = 1\nTicker = 0\nCompatibilityContext = 0\nCanForceOptimusCompatibility = 1\nkeyOsuLeft = B\nkeyOsuRight = N\nkeyOsuSmoke = OemComma\nkeyFruitsDash = LeftShift\nkeyFruitsLeft = Left\nkeyFruitsRight = Right\nkeyTaikoInnerLeft = X\nkeyTaikoInnerRight = C\nkeyTaikoOuterLeft = Z\nkeyTaikoOuterRight = V\nkeyPause = OemPeriod\nkeySkip = Space\nkeyToggleScoreboard = Tab\nkeyToggleChat = F8\nkeyToggleExtendedChat = F9\nkeyScreenshot = F12\nkeyIncreaseAudioOffset = PageUp\nkeyDecreaseAudioOffset = PageDown\nkeyQuickRetry = OemTilde\nkeyIncreaseSpeed = F4\nkeyDecreaseSpeed = F3\nkeyToggleFrameLimiter = F7\nkeyVolumeIncrease = Up\nkeyVolumeDecrease = Down\nkeyDisableMouseButtons = F10\nkeyBossKey = Insert\nkeySelectTool = D1\nkeyNormalTool = D2\nkeySliderTool = D3\nkeySpinnerTool = D4\nkeyNewComboToggle = Q\nkeyWhistleToggle = W\nkeyFinishToggle = E\nkeyClapToggle = R\nkeyGridSnapToggle = T\nkeyDistSnapToggle = Y\nkeyNoteLockToggle = L\nkeyNudgeLeft = J\nkeyNudgeRight = K\nkeyHelpToggle = H\nkeyJumpToBegin = Z\nkeyPlayFromBegin = X\nkeyAudioPause = C\nkeyJumpToEnd = V\nkeyGridChange = G\nkeyTimingSection = None\nkeyInheritingSection = None\nkeyRemoveSection = None\nkeyEasy = Q\nkeyNoFail = W\nkeyHalfTime = E\nkeyHardRock = A\nkeySuddenDeath = S\nkeyDoubleTime = D\nkeyHidden = F\nkeyFlashlight = G\nkeyRelax = Z\nkeyAutopilot = X\nkeySpunOut = C\nkeyAuto = V\nkeyScoreV2 = B\n"
  },
  {
    "path": "tests/game-simulation/.eslintrc.json",
    "content": "{\n  \"extends\": [\n    \"../../.eslintrc.json\"\n  ],\n  \"ignorePatterns\": [\n    \"!**/*\"\n  ],\n  \"overrides\": [\n    {\n      \"files\": [\n        \"*.ts\",\n        \"*.tsx\",\n        \"*.js\",\n        \"*.jsx\"\n      ],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\n        \"*.ts\",\n        \"*.tsx\"\n      ],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\n        \"*.js\",\n        \"*.jsx\"\n      ],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/game-simulation/README.md",
    "content": "Integration tests for `@osujs/(core|pp|math)` that require the \"Rewind test-data\" (see the Wiki on how to download them)\n.\n\nYou can run all testcases, but it takes a while since some beatmaps are just massive.\n\n```shell\nnx run tests-game-simulation:test\n```\n\nIf you have a proper IDE, it is easy to just run individual testcases by using the buttons on the sidebars.\n"
  },
  {
    "path": "tests/game-simulation/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"integration-tests-game-simulation\",\n  preset: \"../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]s$\": \"ts-jest\",\n  },\n  // For some reason we can't have this line in the global jest.config.js - need to investigate later\n  setupFiles: [\"dotenv/config\"],\n  moduleFileExtensions: [\"ts\", \"js\", \"html\"],\n  coverageDirectory: \"../../coverage/apps/integration-tests/game-simulation\",\n};\n"
  },
  {
    "path": "tests/game-simulation/src/core/BeatmapBuilder.spec.ts",
    "content": "import { buildBeatmap, HitCircle, Slider } from \"@osujs/core\";\nimport { assertPositionEqual, parseBlueprintFromFS } from \"../others\";\nimport { TEST_MAPS } from \"../util\";\n\ntest(\"Simple short slider\", function () {\n  const bluePrint = parseBlueprintFromFS(TEST_MAPS.ONE_SLIDER);\n  const hitObjects = buildBeatmap(bluePrint, { addStacking: false, mods: [] }).hitObjects;\n\n  // Only one slider is built\n  expect(hitObjects.length).toBe(1);\n\n  const slider = hitObjects[0] as Slider;\n  // console.log(slider.path.controlPoints.map((p) => p.offset));\n\n  // Actual positions, not offsets\n  assertPositionEqual(slider.ballPositionAt(0), { x: 287, y: 147 });\n  assertPositionEqual(slider.ballPositionAt(0.5), { x: 242.49834839505502, y: 141.5880078631909 });\n  assertPositionEqual(slider.ballPositionAt(1.0), { x: 198.4392805888815, y: 149.81550051795287 });\n\n  // Only legacy last tick\n  expect(slider.checkPoints.length).toBe(1);\n\n  // End time = 1918\n  expect(slider.endTime).toBeCloseTo(1918.375, 2);\n  // This was tested against osu!lazer version\n  expect(slider.checkPoints[0].hitTime).toBeCloseTo(1882.375, 2);\n});\n\ntest(\"Simple slider with repeat\", function () {\n  const bluePrint = parseBlueprintFromFS(TEST_MAPS.SLIDER_WITH_ONE_REPEAT);\n  const beatmap = buildBeatmap(bluePrint, { addStacking: false });\n  const hitObject = beatmap.hitObjects[0];\n  expect(hitObject).toBeInstanceOf(Slider);\n  const slider = hitObject as Slider;\n  expect(slider.repeatCount).toBe(1);\n  expect(slider.checkPoints.length).toBe(2);\n  expect(slider.checkPoints[0].type).toBe(\"REPEAT\");\n  expect(slider.checkPoints[1].type).toBe(\"LAST_LEGACY_TICK\");\n});\n\ntest(\"Short kick slider\", function () {\n  const bluePrint = parseBlueprintFromFS(TEST_MAPS.SHORT_KICK_SLIDER);\n  const beatmap = buildBeatmap(bluePrint, { addStacking: false });\n\n  const kickSlider = beatmap.hitObjects[0] as Slider;\n\n  // KickSlider time interval : [1684,1715.25]\n  expect(kickSlider.startTime).toBeCloseTo(1684);\n  expect(kickSlider.endTime).toBeCloseTo(1715.25);\n\n  expect(kickSlider.repeatCount).toBe(0);\n  expect(kickSlider.checkPoints.length).toBe(1);\n  expect(kickSlider.checkPoints[0].type).toBe(\"LAST_LEGACY_TICK\");\n\n  // The span time interval is shorter than 34ms. Here the legacy last tick is in the center of the time interval.\n  expect(kickSlider.checkPoints[0].hitTime).toBeCloseTo(1699.625, 2);\n});\n\ndescribe(\"Violet Perfume / Map with only HitCircles\", function () {\n  const bluePrint = parseBlueprintFromFS(TEST_MAPS.VIOLET_PERFUME);\n  const beatmap = buildBeatmap(bluePrint);\n  // This map only consists of hit circles\n  const hitCircles = beatmap.hitObjects as HitCircle[];\n  it(\"should build correctly\", function () {\n    expect(beatmap).toBeDefined();\n  });\n  it(\"should assign combo indexes correctly\", function () {\n    expect(hitCircles[0].comboSetIndex).toBe(0);\n    expect(hitCircles[0].withinComboSetIndex).toBe(0);\n\n    expect(hitCircles[1].comboSetIndex).toBe(0);\n    expect(hitCircles[1].withinComboSetIndex).toBe(1);\n\n    expect(hitCircles[4].comboSetIndex).toBe(1);\n    expect(hitCircles[4].withinComboSetIndex).toBe(0);\n  });\n  it(\"should apply stacking correctly\", function () {\n    // They are stacked at position (312, 250) -> see Blueprint of this map\n    expect(hitCircles[7].position).not.toEqual(hitCircles[8].position);\n    expect(hitCircles[8].position).not.toEqual(hitCircles[9].position);\n  });\n});\n\ndescribe(\"Gera Gera / Tech map\", function () {\n  const bluePrint = parseBlueprintFromFS(TEST_MAPS.GERA_GERA);\n  const beatmap = buildBeatmap(bluePrint, { addStacking: false });\n  it(\"should build correctly\", function () {\n    expect(beatmap).toBeDefined();\n  });\n  it(\"should assign combo index correctly\", function () {\n    for (const h of beatmap.hitObjects) {\n      if (h instanceof Slider) {\n        expect(h.head.comboSetIndex).not.toBeNaN();\n        // console.log(`${h.head.comboSetIndex}/${h.head.withinComboSetIndex}`);\n      }\n    }\n  });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/BlueprintParser.spec.ts",
    "content": "import { parseBlueprintFromFS } from \"../others\";\nimport { SliderSettings } from \"@osujs/core\";\nimport { TEST_MAPS } from \"../util\";\n\ndescribe(\"Parsing one slider\", function () {\n  it(\"should parse the one slider correctly\", function () {\n    const bluePrint = parseBlueprintFromFS(TEST_MAPS.ONE_SLIDER);\n    expect(bluePrint.hitObjectSettings.length).toBe(1);\n  });\n});\n\ndescribe(\"Parsing one slider with repeat\", function () {\n  it(\"should parse the one slider with repeat correctly\", function () {\n    const bluePrint = parseBlueprintFromFS(TEST_MAPS.SLIDER_WITH_ONE_REPEAT);\n    expect(bluePrint.hitObjectSettings.length).toBe(1);\n    const sliderSetting = bluePrint.hitObjectSettings[0] as SliderSettings;\n    expect(sliderSetting.repeatCount).toBe(1);\n  });\n});\n\n//\ndescribe(\"Parsing kick slider\", function () {\n  const bluePrint = parseBlueprintFromFS(TEST_MAPS.SHORT_KICK_SLIDER);\n  // TODO: Make a better version of this test because previously there was no flush pending points\n  it(\"should parse the control points correctly\", function () {\n    expect(bluePrint.controlPointInfo.timingPoints.length).toBe(1);\n  });\n});\n\ntest(\"Parsing TSTMTS\", function () {\n  // ~250ms parsing\n  const bluePrint = parseBlueprintFromFS(TEST_MAPS.SUN_MOON_STAR);\n  const numberOfHitObjects = bluePrint.hitObjectSettings.length;\n  expect(numberOfHitObjects).toBe(6953);\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/OsuStdReplayState.spec.ts",
    "content": "import {\n  defaultStableSettings,\n  evaluateWholeReplay,\n  osuClassicScoreScreenJudgementCount,\n  parseReplayFramesFromFS,\n} from \"../others\";\nimport { BucketedGameStateTimeMachine, GameState, HitCircleVerdict, Slider } from \"@osujs/core\";\nimport { TEST_MAPS, testReplayPath } from \"../util\";\n\n/**\n * Info on [Slider 1]\n * One slider:\n * HitTime = 1684\n */\n\ndescribe(\"Daijobanai [Slider (Repeat = 1)]\", function () {\n  const { evaluator, hitObjects } = defaultStableSettings(TEST_MAPS.SLIDER_WITH_ONE_REPEAT);\n  // console.log(\"Slider with repeat = 1\", hitObjects[0]);\n  const sliderHeadId = \"0/HEAD\",\n    repeatId = \"0/0\",\n    lastTickId = \"0/1\";\n\n  describe(\"- Perfume - Daijobanai [Slider (Repeat = 1)] (2021-07-07) Perfect.osr\", function () {\n    const replay = parseReplayFramesFromFS(\n      testReplayPath(\"- Perfume - Daijobanai [Slider (Repeat = 1)] (2021-07-07) Perfect.osr\"),\n    );\n    const state = evaluateWholeReplay(evaluator, replay);\n    it(\"slider head circle must be hit\", function () {\n      expect(state.hitCircleVerdict[sliderHeadId]?.type).not.toBe(\"MISS\");\n    });\n    it(\"repeat and last tick must be hit\", function () {\n      expect(state.checkPointVerdict[repeatId]?.hit).toBe(true);\n      expect(state.checkPointVerdict[lastTickId]?.hit).toBe(true);\n    });\n  });\n});\n\n/**\n * Info on [Short kick slider]\n *\n * hitTime: 1684,\n * endTime: 1715.25\n */\ndescribe(\"Daijobanai [Short kick slider]\", function () {\n  const { evaluator, hitObjects } = defaultStableSettings(TEST_MAPS.SHORT_KICK_SLIDER);\n  const slider = hitObjects[0] as Slider;\n\n  // console.log(\"Short kick slider\", slider.id, slider.head.hitTime, slider.endTime, slider.endPosition);\n  const sliderHeadId = \"0/HEAD\",\n    lastTickId = \"0/0\";\n\n  describe(\"- Perfume - Daijobanai [Short kick slider] (2021-07-16) Perfect.osr\", function () {\n    const replay = parseReplayFramesFromFS(\n      testReplayPath(\"- Perfume - Daijobanai [Short kick slider] (2021-07-16) Perfect.osr\"),\n    );\n    const state = evaluateWholeReplay(evaluator, replay);\n    it(\"slider head circle must be hit\", function () {\n      expect(state.hitCircleVerdict[sliderHeadId]?.type).not.toBe(\"MISS\");\n    });\n    it(\"last tick must be missed\", function () {\n      expect(state.checkPointVerdict[lastTickId]?.hit).toBe(true);\n    });\n  });\n\n  describe(\"- Perfume - Daijobanai [Short kick slider] (2021-07-16) TooLateMissed.osr\", function () {\n    const replay = parseReplayFramesFromFS(\n      testReplayPath(\"- Perfume - Daijobanai [Short kick slider] (2021-07-16) TooLateMissed.osr\"),\n    );\n    const state = evaluateWholeReplay(evaluator, replay);\n    it(\"slider head circle must have been missed due to slider too short force miss\", function () {\n      const actualState = state.hitCircleVerdict[sliderHeadId];\n      const expectedState: HitCircleVerdict = {\n        judgementTime: 1715.25,\n        missReason: \"SLIDER_FINISHED_FASTER\",\n        type: \"MISS\",\n      };\n      expect(actualState).toEqual(expectedState);\n    });\n    it(\"last tick must be missed\", function () {\n      expect(state.checkPointVerdict[lastTickId]?.hit).toBe(false);\n    });\n  });\n});\n\ndescribe(\"OsuStd! ReplayState - Violet Perfume (no sliders/spinners)\", function () {\n  const { hitObjects, settings, evaluator, beatmap, hitWindows } = defaultStableSettings(TEST_MAPS.VIOLET_PERFUME);\n  const replay = parseReplayFramesFromFS(\n    testReplayPath(\"abstrakt - SHK - Violet Perfume [Insane] (2021-03-27) Osu.osr\"),\n  );\n  // console.log(hitWindows);\n\n  const finalState = evaluateWholeReplay(evaluator, replay);\n\n  function count(state: GameState) {\n    return osuClassicScoreScreenJudgementCount(state, hitObjects);\n  }\n\n  // console.log(state.unnecessaryClicks);\n\n  it(\"should be 544x300s, 22x100s, 7x50s and 4 misses at the end\", function () {\n    expect(count(finalState)).toEqual([544, 22, 7, 4]);\n  });\n\n  describe(\"OsuReplayState TimeMachine\", function () {\n    const timeMachine = new BucketedGameStateTimeMachine(replay, beatmap, settings);\n\n    // Not 100% sure, only checked with VLC\n    it(\"at t=5s should be [23, 0, 0, 0]\", function () {\n      const at5 = [23, 0, 0, 0];\n      expect(count(timeMachine.gameStateAt(5 * 1000))).toEqual(at5);\n    });\n\n    // Not 100% sure, only checked with VLC\n    it(\"at t=52s should be [267, 6, 6, 1]\", function () {\n      const at52 = [267, 6, 6, 1];\n      expect(count(timeMachine.gameStateAt(52 * 1000))).toEqual(at52);\n\n      // Just a small test if it actually goes back properly to an unmodified state with immerjs\n      const at5 = [23, 0, 0, 0];\n      expect(count(timeMachine.gameStateAt(5 * 1000))).toEqual(at5);\n    });\n\n    // Basically t=300s exceeds the map's duration, but that should also work in this case\n    it(\"at t=300s should be [544, 22, 7, 4]\", function () {\n      const atEnd = [544, 22, 7, 4]; // This is the only one that is 100% correct\n      expect(count(timeMachine.gameStateAt(300 * 1000))).toEqual(atEnd);\n    });\n  });\n});\n\n// This case is possible:\n\n// SliderHead not hit\n// All SliderTicks before last legacy tick hit\n// HitSliderHead before last legacy tick\n// Hit last legacy tick\n"
  },
  {
    "path": "tests/game-simulation/src/core/ReplayClicks.spec.ts",
    "content": "import { parseReplayFramesFromFS } from \"../others\";\nimport { testReplayPath } from \"../util\";\nimport { calculateReplayClicks } from \"@osujs/core\";\n\ndescribe(\"calculateReplayClicks\", function () {\n  it(\"should calculate clicks correctly for replay frames that have a click starting at 0\", function () {\n    const replay = parseReplayFramesFromFS(\n      testReplayPath(\"kellad - Asriel - Raison D'etre [EXist] (2025-02-16) Osu.osr\"),\n    );\n    const replayClicks = calculateReplayClicks(replay);\n    expect(replayClicks.map((r) => r.length)).toEqual([839, 878]);\n  });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/archive/reference/DANZEN.spec.ts",
    "content": "// abstrakt - Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) [Couch Mini2a] (2021-04-09) Osu.json\n\n// import { compareTimeMachineWithReference } from \"../../../../../libs/osu/core/test/utils/reference\";\nimport { testBlueprintPath, testReferencePath, testReplayPath } from \"../../../util\";\n\nconst blueprintFile = testBlueprintPath(\n  \"840260 Gojou Mayumi - DANZEN! Futari wa PreCure Ver MaxHeart (TV Size)/Gojou Mayumi - DANZEN! Futari wa PreCure Ver. MaxHeart (TV Size) (Sotarks) [Insane].osu\",\n);\nconst replayFile = testReplayPath(\n  \"abstrakt - Gojou Mayumi - DANZEN! Futari wa PreCure Ver. MaxHeart (TV Size) [Insane] (2021-08-21) Osu.osr\",\n);\n// osu!stable\nconst referenceFile = testReferencePath(\n  \"abstrakt - Gojou Mayumi - DANZEN! Futari wa PreCure Ver. MaxHeart (TV Size) [Insane] (2021-08-21) Osu.json\",\n);\n\n// TODO: Delete or refactor\ntest.skip(\"DANZEN\", async () => {\n  // const reference = readStableReferenceJson(referenceFile);\n  // const timeMachine = createTestTimeMachine(blueprintFile, replayFile);\n  // We only check for misses\n  // await compareTimeMachineWithReference(blueprintFile, replayFile, referenceFile, { countsToCheck: [1] });\n  // 1st one: slider end miss\n  // TODO: 2nd one: probably not pressing ??? otherwise massive discrepancy\n  // 3rd one: like 1st one\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/archive/reference/KokoSokoMini.spec.ts",
    "content": "// abstrakt - Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) [Couch Mini2a] (2021-04-09) Osu.json\n\nimport { testBlueprintPath, testReferencePath, testReplayPath } from \"../../../util\";\nimport { compareTimeMachineWithReference } from \"../../../reference\";\n\nconst blueprintFile = testBlueprintPath(\n  \"1302792 Smiledk - Koko Soko (AKIBA KOUBOU Eurobeat Remix)/Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) ([ Couch ] Mini) [Couch Mini2a].osu\",\n);\nconst replayFile = testReplayPath(\n  \"abstrakt - Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) [Couch Mini2a] (2021-04-09) Osu.osr\",\n);\n// osu!stable\nconst referenceFile = testReferencePath(\n  \"abstrakt - Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) [Couch Mini2a] (2021-04-09) Osu.json\",\n);\n\n// TODO: Delete or refactor this test\ntest.skip(\"Koko Soko Mini\", async () => {\n  // const reference = readStableReferenceJson(referenceFile);\n  // const timeMachine = createTestTimeMachine(blueprintFile, replayFile);\n  // We only check for misses\n  await compareTimeMachineWithReference(blueprintFile, replayFile, referenceFile, { countsToCheck: [3] });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/archive/replays/DaijobanaiSlider1.spec.ts",
    "content": "import { defaultStableSettings, evaluateWholeReplay, parseReplayFramesFromFS } from \"../../../others\";\nimport {\n  CheckPointState,\n  GameState,\n  GameStateEvaluator,\n  HitCircleVerdict,\n  MainHitObjectVerdict,\n  OsuHitObject,\n  ReplayFrame,\n  Slider,\n} from \"@osujs/core\";\nimport { TEST_MAPS, testReplayPath } from \"../../../util\";\n\nfunction expectHitCircleToBeNotAMiss(hitCircleState?: HitCircleVerdict) {\n  expect(hitCircleState).toBeDefined();\n  const type = (hitCircleState as HitCircleVerdict).type;\n  expect(type).not.toBe(\"MISS\" as MainHitObjectVerdict);\n}\n\nconst before = beforeEach;\n\ndescribe(\"Daijobanai [Slider 1]\", function () {\n  let evaluator: GameStateEvaluator;\n  let hitObjects: OsuHitObject[];\n  let replay: ReplayFrame[];\n  let state: GameState;\n  before(function () {\n    const s = defaultStableSettings(TEST_MAPS.ONE_SLIDER);\n    hitObjects = s.hitObjects;\n    evaluator = s.evaluator;\n    // console.log(\"Read stable settings \");\n  });\n\n  // console.log(hitObjects[0]);\n  //\n  describe(\"Perfect.osr\", function () {\n    before(function () {\n      replay = parseReplayFramesFromFS(testReplayPath(\"- Perfume - Daijobanai [Slider 1] (2021-07-07) Perfect.osr\"));\n      state = evaluateWholeReplay(evaluator, replay);\n    });\n    it(\"hit circle must be hit correctly\", function () {\n      const hitCircleState = state.hitCircleVerdict[\"0/HEAD\"] as HitCircleVerdict;\n      // hitTime is 1684 and offset was -14, so 1684-14=1670\n      const expectedState: HitCircleVerdict = { type: \"GREAT\", judgementTime: 1670 };\n      expect(hitCircleState).toEqual(expectedState);\n    });\n    it(\"legacy slider tick must be hit\", function () {\n      const sliderTickState = state.checkPointVerdict[\"0/0\"] as CheckPointState;\n      expect(sliderTickState.hit).toBe(true);\n    });\n    it(\"slider verdict should be GREAT\", function () {\n      expect(state.sliderVerdict[\"0\"]).toBe(\"GREAT\");\n    });\n    // This gives a \"300\" as a judgment and a combo of 2\n  });\n\n  describe(\"SliderHeadMissedButTrackingWtf.osr\", function () {\n    before(function () {\n      replay = parseReplayFramesFromFS(\n        testReplayPath(\"- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderHeadMissedButTrackingWtf.osr\"),\n      );\n      state = evaluateWholeReplay(evaluator, replay);\n    });\n    it(\"hit circle missed\", function () {\n      const hitCircleState = state.hitCircleVerdict[\"0/HEAD\"];\n      const expectedState: HitCircleVerdict = {\n        judgementTime: 1804, // 1803 or 1804 TODO: ???\n        type: \"MISS\",\n        missReason: \"TIME_EXPIRED\",\n      };\n      expect(hitCircleState).toEqual(expectedState);\n    });\n    it(\"legacy slider tick must be hit\", function () {\n      const sliderTickState = state.checkPointVerdict[\"0/0\"];\n      expect(sliderTickState.hit).toBe(true);\n    });\n    // This gives a \"50\" as a judgment and a combo of 1\n  });\n\n  describe(\"SliderHeadTooEarly.osr\", function () {\n    before(function () {\n      replay = parseReplayFramesFromFS(\n        testReplayPath(\"- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderHeadTooEarly.osr\"),\n      );\n      state = evaluateWholeReplay(evaluator, replay);\n    });\n    it(\"hit circle hit\", function () {\n      const hitCircleState = state.hitCircleVerdict[\"0/HEAD\"];\n      expect(hitCircleState.judgementTime).toBe(1684 - 57);\n    });\n    it(\"legacy slider tick hit\", function () {\n      const sliderTickState = state.checkPointVerdict[\"0/0\"];\n      expect(sliderTickState.hit).toBe(true);\n    });\n  });\n  describe(\"SliderHeadTooLate.osr\", function () {\n    beforeEach(function () {\n      replay = parseReplayFramesFromFS(\n        testReplayPath(\"- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderHeadTooLate.osr\"),\n      );\n      state = evaluateWholeReplay(evaluator, replay);\n    });\n\n    it(\"hit circle hit\", function () {\n      const hitCircleState = state.hitCircleVerdict[\"0/HEAD\"];\n      expectHitCircleToBeNotAMiss(hitCircleState);\n      expect(hitCircleState.judgementTime).toBe(1684 + 33);\n    });\n    it(\"legacy slider tick hit\", function () {\n      const sliderTickState = state.checkPointVerdict[\"0/0\"];\n      expect(sliderTickState.hit).toBe(true);\n    });\n\n    it(\"slider verdict should be GREAT\", function () {\n      expect(state.sliderVerdict[\"0\"]).toBe(\"GREAT\");\n    });\n  });\n  describe(\"SliderEndMissed.osr\", function () {\n    beforeEach(function () {\n      replay = parseReplayFramesFromFS(\n        testReplayPath(\"- Perfume - Daijobanai [Slider 1] (2021-07-07) SliderEndMissed.osr\"),\n      );\n      state = evaluateWholeReplay(evaluator, replay);\n    });\n\n    it(\"hit circle hit\", function () {\n      const hitCircleState = state.hitCircleVerdict[\"0/HEAD\"];\n      expectHitCircleToBeNotAMiss(hitCircleState);\n    });\n\n    it(\"legacy slider tick missed\", function () {\n      const sliderTickState = state.checkPointVerdict[\"0/0\"];\n      expect(sliderTickState.hit).toBe(false);\n    });\n\n    it(\"slider verdict should be OK\", function () {\n      expect(state.sliderVerdict[\"0\"]).toBe(\"OK\");\n    });\n\n    // This gives a \"100\" as a judgment and a combo of 1\n  });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/archive/replays/sunMoonStar.spec.ts",
    "content": "import {\n  createTestTimeMachine,\n  defaultStableSettings,\n  osuClassicScoreScreenJudgementCount,\n  parseReplayFramesFromFS,\n} from \"../../../others\";\n\nimport { readSync } from \"node-osr\";\nimport {\n  BucketedGameStateTimeMachine,\n  isSlider,\n  osuStableAccuracy,\n  parseReplayFramesFromRaw,\n  Slider,\n} from \"@osujs/core\";\nimport { TEST_MAPS, TEST_REPLAYS } from \"../../../util\";\n\ndescribe(\"Parsing SunMoonStar\", function () {\n  const r = parseReplayFramesFromFS(TEST_REPLAYS.SUN_MOON_STAR_VARVALIAN);\n\n  // .osr 336KB\n  // OsrReplay 1.5MB\n  // ReplayFrames 5.2MB\n  // Parsing + Read took exactly 1s\n  // console.log(JSON.stringify(r).length);\n\n  it(\"should not have duplicated frames\", function () {\n    const seen: Record<number, boolean> = {};\n    for (const frame of r) {\n      expect(frame.time in seen).toBeFalsy();\n      seen[frame.time] = true;\n    }\n  });\n});\n\ntest.skip(\"Testing frame times\", function () {\n  const { timeMachine, beatmap, replay } = createTestTimeMachine(\n    TEST_MAPS.SUN_MOON_STAR,\n    TEST_REPLAYS.SUN_MOON_STAR_VARVALIAN,\n  );\n\n  const i = beatmap.hitObjects.findIndex((o) => isSlider(o) && o.startTime === 876199);\n  const slider = beatmap.hitObjects[i] as Slider;\n\n  // commonStats(replay.frames, 36);\n  // 876399\n});\n\ntest.skip(\"FromRawToReplay Speed test\", function () {\n  console.time(\"readSync\");\n  const a = readSync(TEST_REPLAYS.SUN_MOON_STAR_VARVALIAN);\n  console.timeEnd(\"readSync\");\n\n  console.time(\"fromRawToReplay\");\n  parseReplayFramesFromRaw(a.replay_data);\n  console.timeEnd(\"fromRawToReplay\");\n});\n\n// describe.skip(\"OsuStd! ReplayTimeMachine - The Sun, The Moon, The Stars\", function () {\n//   const { hitObjects, settings, beatmap, hitWindows } = defaultStableSettings(TEST_MAPS.SUN_MOON_STAR);\n//   const frames = parseReplayFramesFromFS(TEST_REPLAYS.SUN_MOON_STAR_VARVALIAN);\n//\n//   // console.log(\"Starting SunMoonStar\");\n//   // console.log(`HitObjects: ${hitObjects.length} Frames: ${frames.length}`);\n//   // console.log(hitWindows);\n//\n//   const timeMachine = new BucketedGameStateTimeMachine(frames, beatmap, settings);\n//   const timeInMs = (min: number, sec: number, ms: number) => ms + sec * 1000 + min * 1000 * 60;\n//\n//   function evaluateForTime(ms: number) {\n//     const state = timeMachine.gameStateAt(ms); // 1:3:866\n//     console.log(\"Evaluating to time \", ms);\n//\n//     // Combo at the time should be ~607\n//     const cnt = osuClassicScoreScreenJudgementCount(state, hitObjects);\n//     const acc = osuStableAccuracy(cnt);\n//     // console.log(`Current Combo: ${state.currentCombo}  (MaxComboSoFar: ${state.maxCombo})`);\n//     console.log(cnt);\n//     if (acc !== undefined) console.log(`Acc: ${acc * 100}%`);\n//\n//     // const dict = normalizeHitObjects(hitObjects);\n//     // for (const [id, s] of state.hitCircleState) {\n//     //   if (s.type !== HitObjectJudgementType.Great) {\n//     //     console.log(\"not great at \", id, s);\n//     //     console.log(dict[id]);\n//     //   }\n//     // }\n//     // for (const [id, s] of state.sliderJudgement) {\n//     //   if (s !== HitObjectJudgementType.Great) {\n//     //     console.log(\"slider not great at id=\", id);\n//     //     console.log(dict[id]);\n//     //   }\n//     // }\n//     // for (const [id, s] of state.hitCircleState) {\n//     //   if (s.type === HitObjectJudgementType.Miss) {\n//     //     console.log(\"missed at \", id, s);\n//     //     console.log(dict[id]);\n//     //   }\n//     // }\n//     // for (const [id, s] of state.sliderJudgement) {\n//     //   if (s === HitObjectJudgementType.Miss) {\n//     //     console.log(\"slider MISS at id=\", id);\n//     //     console.log(dict[id]);\n//     //   }\n//     // }\n//   }\n//\n//   /**\n//    * Somewhat output:\n//    * HitObjects: 6953 Frames: 79946\n//    [ 22, 63, 104, 399 ]\n//    Evaluating to time  63866\n//    Current Combo: 608  (MaxComboSoFar: 608)\n//    [ 480, 2, 0, 0 ]\n//    Acc: 99.72337482710927%\n//    Evaluating to time  112000\n//    Current Combo: 1057  (MaxComboSoFar: 1057)\n//    [ 836, 13, 0, 0 ]\n//    Acc: 98.97919120533962%\n//    Evaluating to time  532000\n//    Current Combo: 4494  (MaxComboSoFar: 4494)\n//    [ 3503, 26, 0, 0 ]\n//    Acc: 99.50883158590725%\n//    Evaluating to time  1072000\n//    Current Combo: 21  (MaxComboSoFar: 8623)\n//    [ 6899, 50, 0, 0 ]\n//    Acc: 99.52031467357413%\n//    */\n//\n//   evaluateForTime(timeInMs(1, 3, 866));\n//   evaluateForTime(timeInMs(1, 52, 0));\n//   evaluateForTime(timeInMs(8, 52, 0));\n//   evaluateForTime(timeInMs(17, 52, 0));\n//   expect(true).toBeTruthy();\n// });\n\n// TODO: Create testcase\n// -> Releasing, but sliderCheckPoint is like before release\n\n// +1.5 is definitely not clean\ntest(\"TSTMTS Reference\", async () => {\n  // const reference = readStableReferenceJson(referenceFile);\n  // const timeMachine = createTestTimeMachine(blueprintFile, replayFile);\n  // We only check for misses\n  // await compareTimeMachineWithReference(TEST_MAPS.SUN_MOON_STAR, TEST_REPLAYS.SUN_MOON_STAR_VARVALIAN, \"\", {\n  // countsToCheck: [3] });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/archive/replays/topranker.spec.ts",
    "content": "import { commonStats, createTestTimeMachine } from \"../../../others\";\nimport { retrieveEvents } from \"@osujs/core\";\nimport { TEST_MAPS, TEST_REPLAYS } from \"../../../util\";\n\n// TODO: Delete or refactor this test\ntest.skip(\"abstrakt - Top Ranker\", () => {\n  // md5 3be542cdcfbad11d922f05f1a7df8463\n  // This replay is a bit bugged:\n  // t=229528\n  // t=229569 -> 41ms difference (almost two frames lost)\n  // There is a slider checkpoint ~229530ms which was not handled. ok!!!\n  const { timeMachine, beatmap, replay } = createTestTimeMachine(\n    TEST_MAPS.TOP_RANKER,\n    TEST_REPLAYS.ABSTRAKT_TOP_RANKER,\n  );\n\n  const gameState = timeMachine.gameStateAt(1e9);\n\n  // commonStats(replay.frames);\n  // There also slider ticks and so on\n  expect(gameState.judgedObjects.length).toBeGreaterThan(1653);\n\n  const events = retrieveEvents(gameState, beatmap.hitObjects);\n  expect(events.length).toBeGreaterThan(1653);\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/bpm.test.ts",
    "content": "import { getBlueprintFromTestDir } from \"../util\";\nimport { buildBeatmap, mostCommonBeatLength } from \"@osujs/core\";\nimport { beatLengthToBPM } from \"@osujs/math\";\n\nconst EXPECTED_PRECISION = 3;\n\ndescribe(\"Most common BPM calculation\", function () {\n  it(\"Akatsuki Zukuyo 180BPM\", function () {\n    const blueprint = getBlueprintFromTestDir(\n      \"351280 HoneyWorks - Akatsuki Zukuyo/HoneyWorks - Akatsuki Zukuyo ([C u r i]) [Taeyang's Extra].osu\",\n    );\n    const beatmap = buildBeatmap(blueprint);\n    expect(\n      beatLengthToBPM(\n        mostCommonBeatLength({\n          hitObjects: beatmap.hitObjects,\n          timingPoints: blueprint.controlPointInfo.timingPoints.list,\n        }) as number,\n      ),\n    ).toBeCloseTo(180, EXPECTED_PRECISION);\n  });\n  it(\"The Unforgiving 134BPM (Range 70-200)\", function () {\n    const blueprint = getBlueprintFromTestDir(\n      \"29157 Within Temptation - The Unforgiving [no video]/Within Temptation - The Unforgiving (Armin) [Marathon].osu\",\n    );\n    const beatmap = buildBeatmap(blueprint);\n    expect(\n      beatLengthToBPM(\n        mostCommonBeatLength({\n          hitObjects: beatmap.hitObjects,\n          timingPoints: blueprint.controlPointInfo.timingPoints.list,\n        }) as number,\n      ),\n    ).toBeCloseTo(134, EXPECTED_PRECISION);\n  });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/core/hitobjects.test.ts",
    "content": "import { readFileSync } from \"fs\";\nimport { Position } from \"@osujs/math\";\nimport { getBlueprintFromTestDir, osuTestData } from \"../util\";\nimport { buildBeatmap, Slider } from \"@osujs/core\";\nimport { toMatchObjectCloseTo } from \"jest-match-object-close-to\";\n\nexpect.extend({ toMatchObjectCloseTo });\n\n/**\n * Tests the generation of the hitobjects against osu!lazer\n *\n * The test case files are generated with https://github.com/abstrakt8/osu\n */\n\ninterface Testsuite {\n  filename: string;\n  sliders: Array<{\n    index: number;\n    duration: number;\n    checkPoints: Array<{\n      type: \"TICK\" | \"REPEAT\" | \"LAST_LEGACY_TICK\";\n      time: number;\n      position: Position;\n    }>;\n  }>;\n}\n\n// TODO: Try to get a precision of 4\nconst expectedPrecision = 3;\n\nfunction runTestSuite({ filename, sliders }: Testsuite) {\n  describe(filename, function () {\n    const blueprint = getBlueprintFromTestDir(filename);\n    const beatmap = buildBeatmap(blueprint, { mods: [] });\n    describe(\"Sliders\", function () {\n      sliders.forEach((lazerSlider) => {\n        describe(lazerSlider.index, function () {\n          const slider = beatmap.hitObjects[lazerSlider.index] as Slider;\n          it(\"number of checkpoints\", function () {\n            expect(slider.checkPoints.length).toBe(lazerSlider.checkPoints.length);\n          });\n          for (let i = 0; i < slider.checkPoints.length; i++) {\n            const actual = slider.checkPoints[i];\n            const expected = lazerSlider.checkPoints[i];\n            describe(`Checkpoint ${i}`, function () {\n              it(\"type\", function () {\n                expect(actual.type).toBe(expected.type);\n              });\n              it(\"time\", function () {\n                expect(actual.hitTime).toBeCloseTo(expected.time, expectedPrecision);\n              });\n              it.only(\"position\", function () {\n                expect(actual.position).toMatchObjectCloseTo(expected.position, expectedPrecision);\n              });\n            });\n          }\n        });\n      });\n    });\n  });\n}\n\ndescribe(\"HitObjects generation\", function () {\n  const data = readFileSync(osuTestData(\"out/hitobjects.json\"), \"utf-8\");\n  const json = JSON.parse(data) as Testsuite[];\n  json.forEach(runTestSuite);\n});\n"
  },
  {
    "path": "tests/game-simulation/src/local/osudb.test.ts",
    "content": "import { readFileSync } from \"fs\";\nimport { OsuDBReader } from \"@rewind/osu-local/db-reader\";\nimport { getOsuGameDir } from \"../util\";\nimport { join } from \"path\";\n\ndescribe(\"OsuDBReader - normal case\", () => {\n  const fileName = join(getOsuGameDir(), \"osu!.db\");\n  const buffer = readFileSync(fileName);\n  const reader = new OsuDBReader(buffer);\n\n  it(\"should parse correctly\", async () => {\n    const osuDB = await reader.readOsuDB();\n    expect(osuDB.osuVersion).toBe(20210820);\n    expect(osuDB.folderCount).toBe(3175);\n    // console.log(osuDB);\n  });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/local/scoresdb.test.ts",
    "content": "import { readFileSync } from \"fs\";\nimport { ScoresDBReader } from \"@rewind/osu-local/db-reader\";\nimport { getOsuGameDir } from \"../util\";\nimport { join } from \"path\";\n\ndescribe(\"ScoresDBReader\", () => {\n  const fileName = join(getOsuGameDir(), \"scores.db\");\n  const buffer = readFileSync(fileName);\n  const reader = new ScoresDBReader(buffer);\n\n  it(\"should parse correctly\", async () => {\n    const scoresDB = await reader.readScoresDB();\n    expect(scoresDB.beatmaps.length).toBeGreaterThan(0);\n    // console.log(scoresDB.beatmaps[0].scores);\n    // console.log(scoresDB.beatmaps[1].scores);\n  });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/others.ts",
    "content": "import * as fs from \"fs\";\nimport { readSync } from \"node-osr\";\nimport {\n  Blueprint,\n  BucketedGameStateTimeMachine,\n  buildBeatmap,\n  defaultGameState,\n  GameState,\n  GameStateEvaluator,\n  GameStateEvaluatorOptions,\n  modsFromBitmask,\n  normalizeHitObjects,\n  OsuClassicMod,\n  OsuHitObject,\n  parseBlueprint,\n  parseReplayFramesFromRaw,\n  ReplayFrame,\n  Slider,\n} from \"@osujs/core\";\nimport { formatGameTime, hitWindowsForOD, Position } from \"@osujs/math\";\nimport { average, max, median, min } from \"simple-statistics\";\n\n// This makes the whole testing module node.js only\n\nexport function assertPositionEqual(actual: Position, expected: Position, numDigits?: number) {\n  expect(actual.x).toBeCloseTo(expected.x, numDigits);\n  expect(actual.y).toBeCloseTo(expected.y, numDigits);\n}\n\nexport function parseBlueprintFromFS(name: string): Blueprint {\n  const data = fs.readFileSync(name);\n  return parseBlueprint(data.toString());\n}\n\nexport function parseReplayFramesFromFS(replayFile: string) {\n  const r = readSync(replayFile);\n  return parseReplayFramesFromRaw(r.replay_data);\n}\n\ninterface TestReplay {\n  frames: ReplayFrame[];\n  mods: OsuClassicMod[];\n}\n\nexport function parseReplayFromFS(replayFile: string): TestReplay {\n  const r = readSync(replayFile);\n\n  return {\n    mods: modsFromBitmask(r.mods),\n    frames: parseReplayFramesFromRaw(r.replay_data),\n  };\n}\n\n// this code  is so messy but should be replaced with something else anyways\nexport function osuClassicScoreScreenJudgementCount(state: GameState, hitObjects: OsuHitObject[], osuLazer?: boolean) {\n  const count = [0, 0, 0, 0];\n  const dict = normalizeHitObjects(hitObjects);\n  const HitObjectVerdicts = {\n    GREAT: 0,\n    OK: 1,\n    MEH: 2,\n    MISS: 3,\n  } as const;\n\n  for (const s of Object.values(state.hitCircleVerdict)) {\n    count[HitObjectVerdicts[s.type]]++;\n  }\n  for (const id in state.sliderVerdict) {\n    const j = state.sliderVerdict[id];\n    count[HitObjectVerdicts[j]]++;\n    const slider = dict[id] as Slider;\n\n    // In osu!classic the heads are not counted and we just subtract them\n    const headState = state.hitCircleVerdict[slider.head.id];\n    if (!headState) throw Error(\"Head state was not calculated?\");\n    count[headState.type]--;\n  }\n\n  return count;\n}\n\nexport function evaluateWholeReplay(evaluator: GameStateEvaluator, replay: ReplayFrame[]) {\n  const state = defaultGameState();\n  for (const frame of replay) {\n    evaluator.evaluate(state, frame);\n  }\n  return state;\n}\n\nexport function defaultStableSettings(mapFile: string) {\n  const blueprint = parseBlueprintFromFS(mapFile);\n  const beatmap = buildBeatmap(blueprint);\n  const hitObjects = beatmap.hitObjects;\n  const hitWindows = hitWindowsForOD(blueprint.defaultDifficulty.overallDifficulty);\n\n  const settings: GameStateEvaluatorOptions = {\n    hitWindowStyle: \"OSU_STABLE\",\n    noteLockStyle: \"STABLE\",\n  };\n\n  const evaluator = new GameStateEvaluator(beatmap, settings);\n\n  return {\n    beatmap,\n    hitObjects,\n    settings,\n    evaluator,\n    hitWindows,\n  };\n}\n\nexport function createTestTimeMachine(mapFile: string, replayFile: string) {\n  const blueprint = parseBlueprintFromFS(mapFile);\n  const replay = parseReplayFromFS(replayFile);\n  const beatmap = buildBeatmap(blueprint, { addStacking: true, mods: replay.mods });\n  const timeMachine = new BucketedGameStateTimeMachine(replay.frames, beatmap, {\n    hitWindowStyle: \"OSU_STABLE\",\n    noteLockStyle: \"STABLE\",\n  });\n\n  return {\n    blueprint,\n    replay,\n    beatmap,\n    timeMachine,\n  };\n}\n\n// Time deltas\nexport function timeDeltas(frames: { time: number }[]) {\n  const deltas: number[] = [];\n  for (let i = 1; i < frames.length; i++) {\n    deltas.push(frames[i].time - frames[i - 1].time);\n  }\n  return deltas;\n}\n\nexport function commonStats(frames: ReplayFrame[], outlierMs = 16 * 2) {\n  const t = timeDeltas(frames);\n  const med = median(t);\n  const avg = average(t);\n  const mn = min(t);\n  const mx = max(t);\n\n  let time = 0;\n  for (let i = 0; i < t.length; i++) {\n    time += t[i];\n    // 2 Frames lost\n    if (t[i] >= outlierMs) {\n      console.log(`Outlier at t=${formatGameTime(time, true)} with delta = ${t[i]}`);\n    }\n  }\n\n  console.log(`Max=${mx} , Min=${mn}, Avg=${avg}, Median=${med}`);\n}\n"
  },
  {
    "path": "tests/game-simulation/src/pp/diff.test.ts",
    "content": "import { Blueprint, buildBeatmap, OsuClassicMod, parseBlueprint } from \"@osujs/core\";\nimport { calculateDifficultyAttributes } from \"@osujs/pp\";\nimport { osuTestData, translateModAcronym } from \"../util\";\n\nimport { toMatchObjectCloseTo } from \"jest-match-object-close-to\";\nimport { readFileSync } from \"fs\";\n\nexpect.extend({ toMatchObjectCloseTo });\n\n/**\n * Tests the SR calculations against the ones generated from the osu!lazer source code (2021-11-14 version).\n */\n\n// n digits after floating point\nconst SR_EXPECTED_PRECISION = 3;\n\ntype TestCase = {\n  mods: string[];\n  starRating: number;\n\n  aimRating: number;\n  speedRating: number;\n  flashlightRating: number;\n};\n\ninterface TestSuite {\n  filename: string;\n  cases: Array<TestCase>;\n}\n\nfunction calculateStarRating(blueprint: Blueprint, mods: OsuClassicMod[] = []) {\n  const beatmap = buildBeatmap(blueprint, { mods });\n  const [lastAttributes] = calculateDifficultyAttributes(beatmap, true);\n  return lastAttributes;\n}\n\nfunction runTestSuite({ filename, cases }: TestSuite) {\n  describe(filename, function () {\n    let blueprint;\n    beforeAll(() => {\n      const data = readFileSync(osuTestData(`Songs/${filename}`), \"utf-8\");\n      blueprint = parseBlueprint(data);\n    });\n    cases.forEach(({ mods: modAcronyms, starRating, speedRating, aimRating, flashlightRating }) => {\n      const testCaseName = modAcronyms.length === 0 ? \"NM\" : modAcronyms.join(\",\");\n      const mods = modAcronyms.map(translateModAcronym);\n      it(testCaseName, function () {\n        const actual = calculateStarRating(blueprint, mods);\n        expect({\n          aimRating: actual.aimDifficulty,\n          speedRating: actual.speedDifficulty,\n          // Test FL with different delta\n          flashlightRating: actual.flashlightDifficulty,\n          starRating: actual.starRating,\n        }).toMatchObjectCloseTo(\n          {\n            aimRating,\n            flashlightRating,\n            speedRating,\n            starRating,\n          },\n          SR_EXPECTED_PRECISION,\n        );\n      });\n    });\n  });\n}\n\ndescribe(\"Star rating calculation\", function () {\n  const data = readFileSync(osuTestData(\"out/sr/20220928.json\"), \"utf-8\");\n  const suites: TestSuite[] = JSON.parse(data);\n  suites.forEach((testCase) => {\n    runTestSuite(testCase);\n  });\n});\n\n// Change `describe.skip` -> `describe.only` to test a specific one.\ndescribe.skip(\"SR a specific one\", function () {\n  const testSuite: TestSuite = {\n    filename: \"SHK - Violet Perfume (ktgster) [Insane].osu\",\n    cases: [\n      {\n        mods: [\"FL\"],\n        starRating: 5.348894637413558,\n        aimRating: 2.285155242240623,\n        speedRating: 2.217866076410721,\n        flashlightRating: 1.3626073031248354,\n      },\n    ],\n  };\n  runTestSuite(testSuite);\n});\n"
  },
  {
    "path": "tests/game-simulation/src/pp/ojsama.test.ts",
    "content": "import { parser, std_diff } from \"ojsama\";\nimport { readFile } from \"fs/promises\";\nimport { TEST_MAPS } from \"../util\";\n\ntest(\"Performance of ojsama\", async () => {\n  const data = await readFile(TEST_MAPS.SUN_MOON_STAR, \"utf-8\");\n  const p = new parser();\n  p.feed(data);\n\n  const map = p.map;\n  const mods = 0;\n\n  const d = new std_diff().calc({ map, mods });\n  // console.log(d.objects.map((d) => d.strains[0] + d.strains[1]));\n  // Around 150ms: pretty fast for 6000 hitobjects\n});\n"
  },
  {
    "path": "tests/game-simulation/src/pp/pp.test.ts",
    "content": "import { Blueprint, buildBeatmap, OsuClassicMod, parseBlueprint } from \"@osujs/core\";\nimport { calculateDifficultyAttributes, calculatePerformanceAttributes } from \"@osujs/pp\";\nimport { osuTestData, translateModAcronym } from \"../util\";\nimport { readFileSync } from \"fs\";\n\n// 3 is possible on maps that don't have extremely obscure sliders\nconst PP_EXPECTED_PRECISION = 2;\n\ntype TestCase = {\n  mods: string[];\n  combo: number;\n  countGreat: number;\n  countOk: number;\n  countMeh: number;\n  countMiss: number;\n  totalPP: number;\n};\n\ninterface TestSuite {\n  filename: string;\n  cases: Array<TestCase>;\n}\n\nfunction calculateStarRating(blueprint: Blueprint, mods: OsuClassicMod[] = []) {\n  const beatmap = buildBeatmap(blueprint, { mods });\n  const [lastAttributes] = calculateDifficultyAttributes(beatmap, true);\n  return lastAttributes;\n}\n\nfunction testItMan(\n  blueprint: Blueprint,\n  { mods: modAcronyms, totalPP: expectedPP, countMeh, countGreat, countOk, countMiss, combo }: TestCase,\n) {\n  const modAcronymsName = modAcronyms.length === 0 ? \"NM\" : modAcronyms.join(\",\");\n  const testCaseName = `${modAcronymsName} ${combo}x ${[countGreat, countOk, countMeh, countMiss]} `;\n  const mods = modAcronyms.map(translateModAcronym);\n  it(testCaseName, function () {\n    const finalAttributes = calculateStarRating(blueprint, mods);\n    const { total } = calculatePerformanceAttributes(finalAttributes, {\n      mods,\n      countMeh,\n      countMiss,\n      countOk,\n      maxCombo: combo,\n      countGreat,\n    });\n    expect(total).toBeCloseTo(expectedPP, PP_EXPECTED_PRECISION);\n  });\n}\nfunction runTestSuite({ filename, cases }: TestSuite) {\n  describe(filename, function () {\n    const data = readFileSync(osuTestData(`Songs/${filename}`), \"utf-8\");\n    const blueprint = parseBlueprint(data);\n    cases.forEach((c) => testItMan(blueprint, c));\n  });\n}\n\ndescribe(\"PP calculation\", function () {\n  const data = readFileSync(osuTestData(\"out/pp/20220928.json\"), \"utf-8\");\n  const suites: TestSuite[] = JSON.parse(data);\n  suites.forEach((testCase) => {\n    runTestSuite(testCase);\n  });\n});\n\ndescribe.skip(\"Just One\", function () {\n  const filename = \"Brian The Sun - Lonely Go! (TV Size) (Nevo) [Fiery's Extreme].osu\";\n  const data = readFileSync(osuTestData(`Songs/${filename}`), \"utf-8\");\n  const blueprint = parseBlueprint(data);\n\n  testItMan(blueprint, {\n    mods: [],\n    combo: 455,\n    countGreat: 341,\n    countOk: 10,\n    countMeh: 0,\n    countMiss: 1,\n    totalPP: 211.23440779362528,\n  });\n});\n"
  },
  {
    "path": "tests/game-simulation/src/reference.ts",
    "content": "import { readFile } from \"fs/promises\";\nimport { createTestTimeMachine } from \"./others\";\nimport { GameplayInfoEvaluator } from \"@osujs/core\";\nimport { formatGameTime } from \"@osujs/math\";\n\n// These files have been generated and are used as a reference for the \"correct\" osu!stable behavior.\n\ninterface ReferenceStructure {\n  beatmapMd5: string;\n  frames: Array<{ time: number; counts: [number, number, number, number] }>;\n  hitOffsets: Array<number>;\n}\n\nexport async function readStableReferenceJson(path: string) {\n  const data = await readFile(path, { encoding: \"utf-8\" });\n  const ref = JSON.parse(data);\n  return ref as ReferenceStructure;\n}\n\n/**\n * @returns result[i] = a[i] - b[i]\n */\nfunction arrayDelta(a: number[], b: number[]) {\n  if (a.length !== b.length) {\n    throw Error(\"The lengths must match!\");\n  }\n  return a.map((val, i) => val - b[i]);\n}\n\nfunction debugGameTime(timeInMs: number) {\n  return `${formatGameTime(timeInMs, true)} (${timeInMs}ms)`;\n}\n\nfunction countsEqual(expected: number[], actual: number[], check: number[] = [0, 1, 2, 3]) {\n  for (const i of check) {\n    if (expected[i] !== actual[i]) {\n      return false;\n    }\n  }\n  return true;\n}\n\ninterface Options {\n  countsToCheck: number[];\n}\n\nconst defaultOptions: Options = {\n  countsToCheck: [0, 1, 2, 3],\n};\n\nexport async function compareTimeMachineWithReference(\n  blueprintFile: string,\n  replayFile: string,\n  referenceFile: string,\n  { countsToCheck }: Options = defaultOptions,\n) {\n  const reference = await readStableReferenceJson(referenceFile);\n  const { timeMachine, beatmap } = createTestTimeMachine(blueprintFile, replayFile);\n  const gameplayEvaluator = new GameplayInfoEvaluator(beatmap, { scoringSystem: \"ScoreV1\" });\n\n  const mismatches = [];\n  for (let i = 1; i < reference.frames.length; i++) {\n    const { time: timePrev, counts: countsPrev } = reference.frames[i - 1];\n    const { time: timeCur, counts: countsCur } = reference.frames[i];\n\n    const timeDelta = timeCur - timePrev;\n    const expectedCountsDelta = arrayDelta(countsCur, countsPrev);\n\n    const { verdictCounts: actualCountsPrev } = gameplayEvaluator.evaluateReplayState(\n      timeMachine.gameStateAt(timePrev),\n    );\n    const { verdictCounts: actualCountsCur } = gameplayEvaluator.evaluateReplayState(timeMachine.gameStateAt(timeCur));\n    const actualCountsDelta = arrayDelta(actualCountsCur, actualCountsPrev);\n\n    if (!countsEqual(expectedCountsDelta, actualCountsDelta, countsToCheck)) {\n      console.log(\n        `Between ${debugGameTime(timePrev)} and ${debugGameTime(\n          timeCur,\n        )} there is a count delta mismatch!\\nExpected: ${expectedCountsDelta} (${countsCur})\\nActual: ${actualCountsDelta} (${actualCountsCur})`,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "tests/game-simulation/src/util.ts",
    "content": "import { join } from \"path\";\nimport { OsuClassicMod, parseBlueprint } from \"@osujs/core\";\nimport { readFileSync } from \"fs\";\nimport * as path from \"node:path\";\n\nfunction getRewindTestDir() {\n  return path.resolve(__dirname, \"../../../testdata\");\n}\n\nexport function getOsuGameDir() {\n  return join(getRewindTestDir(), \"osu!\");\n}\n\nexport function blueprintPath(file: string) {\n  return join(getRewindTestDir(), \"osu!\", \"Songs\", file);\n}\n\nexport function osuTestData(file: string) {\n  return join(getRewindTestDir(), \"osu-testdata\", file);\n}\n\n// Move to osu core\nexport function translateModAcronym(acronym: string): OsuClassicMod {\n  switch (acronym) {\n    case \"HD\":\n      return \"HIDDEN\";\n    case \"HR\":\n      return \"HARD_ROCK\";\n    case \"HT\":\n      return \"HALF_TIME\";\n    case \"DT\":\n      return \"DOUBLE_TIME\";\n    case \"FL\":\n      return \"FLASH_LIGHT\";\n    case \"EZ\":\n      return \"EASY\";\n    case \"NF\":\n      return \"NO_FAIL\";\n    case \"SO\":\n      return \"SPUN_OUT\";\n    case \"RX\":\n      return \"RELAX\";\n  }\n  throw Error(`Acronym ${acronym} not known`);\n}\n\nexport function getBlueprintFromTestDir(file: string) {\n  const data = readFileSync(blueprintPath(file), \"utf-8\");\n  return parseBlueprint(data);\n}\n\nexport function testBlueprintPath(fileName: string): string {\n  return blueprintPath(fileName);\n}\n\nexport function testReplayPath(fileName: string): string {\n  return join(getOsuGameDir(), \"Replays\", fileName);\n}\n\nexport function testReferencePath(fileName: string): string {\n  // TODO rewindReferenceDir\n  return join(\"\", fileName);\n}\n\n// TODO: Maybe move those to the respective test suites\n\nexport const TEST_MAPS = {\n  ONE_SLIDER: testBlueprintPath(\"967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Slider 1].osu\"),\n  SLIDER_WITH_ONE_REPEAT: testBlueprintPath(\n    \"967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Slider (Repeat = 1)].osu\",\n  ),\n  SHORT_KICK_SLIDER: testBlueprintPath(\n    \"967347 Perfume - Daijobanai/Perfume - Daijobanai (eiri-) [Short kick slider].osu\",\n  ),\n  VIOLET_PERFUME: testBlueprintPath(\n    \"1010865 SHK - Violet Perfume [no video]/SHK - Violet Perfume (ktgster) [Insane].osu\",\n  ),\n  GERA_GERA: testBlueprintPath(\n    \"1001507 ZUTOMAYO - Kan Saete Kuyashiiwa/ZUTOMAYO - Kan Saete Kuyashiiwa (Nathan) [geragera].osu\",\n  ),\n  SUN_MOON_STAR: testBlueprintPath(\n    \"933630 Aether Realm - The Sun, The Moon, The Star/Aether Realm - The Sun, The Moon, The Star (ItsWinter) [Mourning Those Things I've Long Left Behind].osu\",\n  ),\n  TOP_RANKER: testBlueprintPath(\n    \"1357624 sabi - true DJ MAG top ranker's song Zenpen (katagiri Remix)/sabi - true DJ MAG top ranker's song Zenpen (katagiri Remix) (Nathan) [Senseabel's Extra].osu\",\n  ),\n};\n\nexport const TEST_REPLAYS = {\n  SUN_MOON_STAR_VARVALIAN: testReplayPath(\n    \"Varvalian - Aether Realm - The Sun, The Moon, The Star [Mourning Those Things I've Long Left Behind] (2019-05-15) Osu.osr\",\n  ),\n  ABSTRAKT_TOP_RANKER: testReplayPath(\n    \"abstrakt - sabi - true DJ MAG top ranker's song Zenpen (katagiri Remix) [Senseabel's Extra] (2021-08-08) Osu.osr\",\n  ),\n  KOKO_SOKO_MINI_ABSTRAKT: testReplayPath(\n    \"abstrakt - Smile.dk - Koko Soko (AKIBA KOUBOU Eurobeat Remix) [Couch Mini2a] (2021-04-09) Osu.osr\",\n  ),\n};\n"
  },
  {
    "path": "tests/game-simulation/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/game-simulation/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"jest\",\n      \"node\"\n    ]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.d.ts\",\n    \"**/*.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "tests/osu-stable-test-generator/.eslintrc.json",
    "content": "{\n  \"extends\": [\"../../.eslintrc.json\"],\n  \"ignorePatterns\": [\"!**/*\"],\n  \"overrides\": [\n    {\n      \"files\": [\"*.ts\", \"*.tsx\", \"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.ts\", \"*.tsx\"],\n      \"rules\": {}\n    },\n    {\n      \"files\": [\"*.js\", \"*.jsx\"],\n      \"rules\": {}\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/osu-stable-test-generator/jest.config.ts",
    "content": "/* eslint-disable */\nexport default {\n  displayName: \"osu-stable-test-generator\",\n  preset: \"../../jest.preset.js\",\n  globals: {\n    \"ts-jest\": {\n      tsconfig: \"<rootDir>/tsconfig.spec.json\",\n    },\n  },\n  testEnvironment: \"node\",\n  transform: {\n    \"^.+\\\\.[tj]s$\": \"ts-jest\",\n  },\n  moduleFileExtensions: [\"ts\", \"js\", \"html\"],\n  coverageDirectory: \"../../coverage/apps/osu-stable-test-generator\",\n};\n"
  },
  {
    "path": "tests/osu-stable-test-generator/src/app/.gitkeep",
    "content": ""
  },
  {
    "path": "tests/osu-stable-test-generator/src/assets/.gitkeep",
    "content": ""
  },
  {
    "path": "tests/osu-stable-test-generator/src/environments/environment.prod.ts",
    "content": "export const environment = {\n  production: true,\n};\n"
  },
  {
    "path": "tests/osu-stable-test-generator/src/environments/environment.ts",
    "content": "export const environment = {\n  production: false,\n};\n"
  },
  {
    "path": "tests/osu-stable-test-generator/src/main.ts",
    "content": "import * as WebSocket from \"ws\";\nimport { GosuMemoryAPI, OsuMemoryStatus } from \"@rewind/osu-local/gosumemory\";\nimport { writeFileSync } from \"fs\";\n\nconst wsUrl = \"ws://localhost:24050/ws\";\n\nconst ws = new WebSocket(wsUrl);\n\nexport interface OsuStableGameState {\n  time: number;\n  counts: number[];\n  hitOffsets: number[]; // hitError\n}\n\nfunction arrayEqual<T>(a: T[], b: T[]) {\n  if (a.length !== b.length) return false;\n  for (let i = 0; i < a.length; i++) {\n    if (a[i] !== b[i]) {\n      return false;\n    }\n  }\n  return true;\n}\n\n//\nexport const currentIsInteresting = (previous: OsuStableGameState, current: OsuStableGameState): boolean => {\n  // const hitOffsetsDifferent = !arrayEqual(previous.hitOffsets, current.hitOffsets) && false; // We don't want them\n  const countsDifferent = !arrayEqual(previous.counts, current.counts);\n  return countsDifferent;\n  // return hitOffsetsDifferent || countsDifferent;\n};\n\ninterface MyData extends OsuStableGameState {\n  state: OsuMemoryStatus;\n  beatmapMd5: string;\n}\n\nconst defaultData: MyData = {\n  beatmapMd5: \"\",\n  state: OsuMemoryStatus.Unknown,\n  time: -1,\n  counts: [],\n  hitOffsets: [],\n};\n\ninterface DataToStore {\n  beatmapMd5: string;\n  frames: { time: number; counts: number[] }[];\n  hitOffsets: number[];\n}\n\nfunction persist(file: string, data: DataToStore) {\n  const json = JSON.stringify(data);\n  writeFileSync(file, json);\n}\n\nfunction filterInteresting(gameStates: OsuStableGameState[]) {\n  // First one always interesting\n  if (gameStates.length === 0) {\n    return [];\n  }\n  const res = [gameStates[0]];\n  for (let i = 1; i < gameStates.length; i++) {\n    if (currentIsInteresting(res[res.length - 1], gameStates[i])) {\n      res.push(gameStates[i]);\n    }\n  }\n  return res;\n}\n\nclass TestGenerator {\n  previousState = OsuMemoryStatus.Unknown;\n  gameStates: OsuStableGameState[] = [];\n\n  processPlaying(data: MyData) {\n    if (this.previousState !== OsuMemoryStatus.Playing) {\n      console.log(`Started with new beatmap ${data.beatmapMd5}`);\n      // `data` is really weird at the beginning\n      // Sometimes it's like time=20000 with count=0,0,0,0 then on next frame it's like time=16ms with count=[0,0,0,1]\n      // That's why we manually initialize this array.\n      this.gameStates = [{ hitOffsets: [], time: 0, counts: [0, 0, 0, 0] }];\n    } else {\n      this.gameStates.push(data);\n    }\n  }\n\n  processResultScreen(data: MyData) {\n    // We only want to store some data if the previous state was actually playing and not something like SongSelect\n    if (this.previousState === OsuMemoryStatus.Playing && this.gameStates.length > 0) {\n      const interesting = filterInteresting(this.gameStates);\n      console.log(\"Going to store ...\", interesting.length);\n      if (interesting.length === 0) {\n        return;\n      }\n\n      const lastState = interesting[interesting.length - 1];\n      const frames = interesting.map((a) => ({ time: a.time, counts: a.counts }));\n      const store: DataToStore = {\n        beatmapMd5: data.beatmapMd5,\n        frames: frames,\n        hitOffsets: lastState.hitOffsets,\n      };\n\n      const name = `${data.beatmapMd5}_${Date.now()}.json`;\n      console.log(`Storing data into file ${name}`);\n      persist(name, store);\n      // console.log(JSON.stringify(store));\n    }\n  }\n\n  processData(data: MyData) {\n    switch (data.state) {\n      case OsuMemoryStatus.ResultsScreen:\n        this.processResultScreen(data);\n        break;\n      case OsuMemoryStatus.Playing:\n        this.processPlaying(data);\n        break;\n    }\n    this.previousState = data.state;\n  }\n}\n\nconst Verdicts = [\"300\", \"100\", \"50\", \"0\"] as const;\ntype Verdict = typeof Verdicts[number];\n\nfunction extractData(data: GosuMemoryAPI): MyData {\n  const { menu, gameplay } = data;\n  return {\n    state: menu.state,\n    time: menu.bm.time.current,\n    counts: Verdicts.map((s) => gameplay.hits[s]),\n    hitOffsets: gameplay.hits.hitErrorArray ?? [],\n    beatmapMd5: menu.bm.md5,\n  };\n}\n\nconst testGenerator = new TestGenerator();\n\n/**\n * If going to results screen -> store as a json\n *\n * md5Hash_timestamp.json\n *\n * @param event\n */\nws.onmessage = (event) => {\n  if (typeof event.data !== \"string\") {\n    return;\n  }\n  const data = JSON.parse(event.data) as GosuMemoryAPI;\n  testGenerator.processData(extractData(data));\n};\n"
  },
  {
    "path": "tests/osu-stable-test-generator/tsconfig.app.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"strict\": true,\n    \"module\": \"commonjs\",\n    \"types\": [\n      \"node\"\n    ]\n  },\n  \"exclude\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"jest.config.ts\"\n  ],\n  \"include\": [\n    \"**/*.ts\"\n  ]\n}\n"
  },
  {
    "path": "tests/osu-stable-test-generator/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"files\": [],\n  \"include\": [],\n  \"references\": [\n    {\n      \"path\": \"./tsconfig.app.json\"\n    },\n    {\n      \"path\": \"./tsconfig.spec.json\"\n    }\n  ]\n}\n"
  },
  {
    "path": "tests/osu-stable-test-generator/tsconfig.spec.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../../dist/out-tsc\",\n    \"module\": \"commonjs\",\n    \"types\": [\"jest\", \"node\"]\n  },\n  \"include\": [\n    \"**/*.spec.ts\",\n    \"**/*.test.ts\",\n    \"**/*.d.ts\",\n    \"jest.config.ts\"\n  ]\n}\n"
  },
  {
    "path": "tools/generators/.gitkeep",
    "content": ""
  },
  {
    "path": "tools/tsconfig.tools.json",
    "content": "{\n  \"extends\": \"../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"outDir\": \"../dist/out-tsc/tools\",\n    \"rootDir\": \".\",\n    \"module\": \"commonjs\",\n    \"target\": \"es5\",\n    \"types\": [\"node\"],\n    \"importHelpers\": false\n  },\n  \"include\": [\"**/*.ts\"]\n}\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"rootDir\": \".\",\n    \"sourceMap\": true,\n    \"declaration\": false,\n    \"moduleResolution\": \"node\",\n    \"emitDecoratorMetadata\": true,\n    \"experimentalDecorators\": true,\n    \"strictNullChecks\": true,\n    \"importHelpers\": true,\n    \"target\": \"es2015\",\n    \"module\": \"esnext\",\n    \"lib\": [\"es2017\", \"dom\"],\n    \"skipLibCheck\": true,\n    \"skipDefaultLibCheck\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@osujs/core\": [\"libs/osu/core/src/index.ts\"],\n      \"@osujs/math\": [\"libs/osu/math/src/index.ts\"],\n      \"@osujs/pp\": [\"libs/osu/pp/src/index.ts\"],\n      \"@rewind/osu-local/db-reader\": [\"libs/osu-local/db-reader/src/index.ts\"],\n      \"@rewind/osu-local/gosumemory\": [\"libs/osu-local/gosumemory/src/index.ts\"],\n      \"@rewind/osu-local/osr-reader\": [\"libs/osu-local/osr-reader/src/index.ts\"],\n      \"@rewind/osu-local/skin-reader\": [\"libs/osu-local/skin-reader/src/index.ts\"],\n      \"@rewind/osu-local/utils\": [\"libs/osu-local/utils/src/index.ts\"],\n      \"@rewind/osu-pixi/classic-components\": [\"libs/osu-pixi/classic-components/src/index.ts\"],\n      \"@rewind/osu-pixi/rewind\": [\"libs/osu-pixi/rewind/src/index.ts\"],\n      \"@rewind/osu/skin\": [\n        \"libs/osu/skin/src/index.ts\"\n      ]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"tmp\"]\n}\n"
  },
  {
    "path": "workspace.json",
    "content": "{\n  \"version\": 2,\n  \"projects\": {\n    \"desktop-frontend\": {\n      \"root\": \"apps/desktop/frontend\",\n      \"sourceRoot\": \"apps/desktop/frontend/src\",\n      \"projectType\": \"application\",\n      \"targets\": {\n        \"build\": {\n          \"executor\": \"@nrwl/web:webpack\",\n          \"outputs\": [\"{options.outputPath}\"],\n          \"options\": {\n            \"outputPath\": \"dist/apps/desktop/frontend\",\n            \"index\": \"apps/desktop/frontend/src/index.html\",\n            \"main\": \"apps/desktop/frontend/src/main.tsx\",\n            \"polyfills\": \"apps/desktop/frontend/src/polyfills.ts\",\n            \"tsConfig\": \"apps/desktop/frontend/tsconfig.app.json\",\n            \"assets\": [\"apps/desktop/frontend/src/favicon.ico\", \"apps/desktop/frontend/src/assets\"],\n            \"styles\": [\"apps/desktop/frontend/src/styles.css\"],\n            \"scripts\": [],\n            \"webpackConfig\": \"apps/desktop/frontend/webpack.config.js\"\n          },\n          \"configurations\": {\n            \"production\": {\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"apps/desktop/frontend/src/environments/environment.ts\",\n                  \"with\": \"apps/desktop/frontend/src/environments/environment.prod.ts\"\n                }\n              ],\n              \"optimization\": true,\n              \"outputHashing\": \"all\",\n              \"sourceMap\": false,\n              \"extractCss\": true,\n              \"namedChunks\": false,\n              \"extractLicenses\": true,\n              \"vendorChunk\": false,\n              \"baseHref\": \"./\",\n              \"budgets\": [\n                {\n                  \"type\": \"initial\",\n                  \"maximumWarning\": \"5mb\",\n                  \"maximumError\": \"10mb\"\n                }\n              ]\n            },\n            \"development\": {\n              \"extractLicenses\": false,\n              \"optimization\": false,\n              \"sourceMap\": true,\n              \"vendorChunk\": true\n            }\n          },\n          \"defaultConfiguration\": \"production\"\n        },\n        \"serve\": {\n          \"executor\": \"@nrwl/web:dev-server\",\n          \"options\": {\n            \"buildTarget\": \"desktop-frontend:build\",\n            \"hmr\": true,\n            \"proxyConfig\": \"apps/desktop/frontend/proxy.conf.json\"\n          },\n          \"configurations\": {\n            \"production\": {\n              \"buildTarget\": \"desktop-frontend:build:production\",\n              \"hmr\": false\n            },\n            \"development\": {\n              \"buildTarget\": \"desktop-frontend:build:development\"\n            }\n          },\n          \"defaultConfiguration\": \"development\"\n        },\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"apps/desktop/frontend/**/*.{ts,tsx,js,jsx}\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/apps/desktop/frontend\"],\n          \"options\": {\n            \"jestConfig\": \"apps/desktop/frontend/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"desktop-main\": {\n      \"root\": \"apps/desktop/main\",\n      \"sourceRoot\": \"apps/desktop/main/src\",\n      \"projectType\": \"application\",\n      \"targets\": {\n        \"build\": {\n          \"executor\": \"@nrwl/node:webpack\",\n          \"outputs\": [\"{options.outputPath}\"],\n          \"options\": {\n            \"outputFileName\": \"index.js\",\n            \"outputPath\": \"dist/apps/desktop\",\n            \"main\": \"apps/desktop/main/src/index.ts\",\n            \"tsConfig\": \"apps/desktop/main/tsconfig.app.json\",\n            \"assets\": [\"apps/desktop/main/src/assets\"]\n          },\n          \"configurations\": {\n            \"production\": {\n              \"optimization\": true,\n              \"extractLicenses\": true,\n              \"inspect\": false,\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"apps/desktop/main/src/environments/environment.ts\",\n                  \"with\": \"apps/desktop/main/src/environments/environment.prod.ts\"\n                }\n              ]\n            }\n          }\n        },\n        \"serve\": {\n          \"executor\": \"@nrwl/node:node\",\n          \"options\": {\n            \"buildTarget\": \"rewind-electron:build\"\n          }\n        },\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"apps/desktop/main/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/apps/desktop-main\"],\n          \"options\": {\n            \"jestConfig\": \"apps/desktop/main/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-core\": {\n      \"root\": \"libs/osu/core\",\n      \"sourceRoot\": \"libs/osu/core/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu/core/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu/core\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu/core/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        },\n        \"build\": {\n          \"executor\": \"@nrwl/js:tsc\",\n          \"outputs\": [\n            \"{options.outputPath}\"\n          ],\n          \"options\": {\n            \"outputPath\": \"dist/libs/osu/core\",\n            \"tsConfig\": \"libs/osu/core/tsconfig.lib.json\",\n            \"packageJson\": \"libs/osu/core/package.json\",\n            \"main\": \"libs/osu/core/src/index.ts\",\n            \"assets\": [\n              \"libs/osu/core/*.md\"\n            ]\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-local-db-reader\": {\n      \"root\": \"libs/osu-local/db-reader\",\n      \"sourceRoot\": \"libs/osu-local/db-reader/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu-local/db-reader/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu-local/db-reader\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu-local/db-reader/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-local-gosumemory\": {\n      \"root\": \"libs/osu-local/gosumemory\",\n      \"sourceRoot\": \"libs/osu-local/gosumemory/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu-local/gosumemory/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu-local/gosumemory\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu-local/gosumemory/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-local-osr-reader\": {\n      \"root\": \"libs/osu-local/osr-reader\",\n      \"sourceRoot\": \"libs/osu-local/osr-reader/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu-local/osr-reader/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu-local/osr-reader\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu-local/osr-reader/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-local-skin-reader\": {\n      \"root\": \"libs/osu-local/skin-reader\",\n      \"sourceRoot\": \"libs/osu-local/skin-reader/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu-local/skin-reader/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu-local/skin-reader\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu-local/skin-reader/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-local-utils\": {\n      \"root\": \"libs/osu-local/utils\",\n      \"sourceRoot\": \"libs/osu-local/utils/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu-local/utils/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu-local/utils\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu-local/utils/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-math\": {\n      \"root\": \"libs/osu/math\",\n      \"sourceRoot\": \"libs/osu/math/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu/math/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu/math\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu/math/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        },\n        \"build\": {\n          \"executor\": \"@nrwl/js:tsc\",\n          \"outputs\": [\n            \"{options.outputPath}\"\n          ],\n          \"options\": {\n            \"outputPath\": \"dist/libs/osu/math\",\n            \"tsConfig\": \"libs/osu/math/tsconfig.lib.json\",\n            \"packageJson\": \"libs/osu/math/package.json\",\n            \"main\": \"libs/osu/math/src/index.ts\",\n            \"assets\": []\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-pixi-classic-components\": {\n      \"root\": \"libs/osu-pixi/classic-components\",\n      \"sourceRoot\": \"libs/osu-pixi/classic-components/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu-pixi/classic-components/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu-pixi/classic-components\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu-pixi/classic-components/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-pixi-rewind\": {\n      \"root\": \"libs/osu-pixi/rewind\",\n      \"sourceRoot\": \"libs/osu-pixi/rewind/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"outputs\": [\"{options.outputFile}\"],\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu-pixi/rewind/**/*.ts\"]\n          }\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu-pixi/rewind\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu-pixi/rewind/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-pp\": {\n      \"root\": \"libs/osu/pp\",\n      \"sourceRoot\": \"libs/osu/pp/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"outputs\": [\"{options.outputFile}\"],\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu/pp/**/*.ts\"]\n          }\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu/pp\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu/pp/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        },\n        \"build\": {\n          \"executor\": \"@nrwl/js:tsc\",\n          \"outputs\": [\n            \"{options.outputPath}\"\n          ],\n          \"options\": {\n            \"outputPath\": \"dist/libs/osu/pp\",\n            \"tsConfig\": \"libs/osu/pp/tsconfig.lib.json\",\n            \"packageJson\": \"libs/osu/pp/package.json\",\n            \"main\": \"libs/osu/pp/src/index.ts\",\n            \"assets\": []\n          }\n        },\n        \"build-web\": {\n          \"executor\": \"@nrwl/web:rollup\",\n          \"outputs\": [\n            \"{options.outputPath}\"\n          ],\n          \"options\": {\n            \"project\": \"libs/osu/pp/package.json\",\n            \"entryFile\": \"libs/osu/pp/src/index.ts\",\n            \"outputPath\": \"dist/libs/osu/pp/dist\",\n            \"deleteOutputPath\": true,\n            \"format\": [\n              \"esm\"\n            ],\n            \"tsConfig\": \"libs/osu/pp/tsconfig.lib.json\",\n            \"assets\": []\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-skin\": {\n      \"root\": \"libs/osu/skin\",\n      \"sourceRoot\": \"libs/osu/skin/src\",\n      \"projectType\": \"library\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"libs/osu/skin/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/libs/osu/skin\"],\n          \"options\": {\n            \"jestConfig\": \"libs/osu/skin/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"osu-stable-test-generator\": {\n      \"root\": \"tests/osu-stable-test-generator\",\n      \"sourceRoot\": \"tests/osu-stable-test-generator/src\",\n      \"projectType\": \"application\",\n      \"targets\": {\n        \"build\": {\n          \"executor\": \"@nrwl/node:webpack\",\n          \"outputs\": [\"{options.outputPath}\"],\n          \"options\": {\n            \"outputPath\": \"dist/tests/osu-stable-test-generator\",\n            \"main\": \"tests/osu-stable-test-generator/src/main.ts\",\n            \"tsConfig\": \"tests/osu-stable-test-generator/tsconfig.app.json\",\n            \"assets\": [\"tests/osu-stable-test-generator/src/assets\"]\n          },\n          \"configurations\": {\n            \"production\": {\n              \"optimization\": true,\n              \"extractLicenses\": true,\n              \"inspect\": false,\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"tests/osu-stable-test-generator/src/environments/environment.ts\",\n                  \"with\": \"tests/osu-stable-test-generator/src/environments/environment.prod.ts\"\n                }\n              ]\n            }\n          }\n        },\n        \"serve\": {\n          \"executor\": \"@nrwl/node:node\",\n          \"options\": {\n            \"buildTarget\": \"osu-stable-test-generator:build\"\n          }\n        },\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"tests/osu-stable-test-generator/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/tests/osu-stable-test-generator\"],\n          \"options\": {\n            \"jestConfig\": \"tests/osu-stable-test-generator/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"tests-game-simulation\": {\n      \"root\": \"tests/game-simulation\",\n      \"sourceRoot\": \"tests/game-simulation/src\",\n      \"projectType\": \"application\",\n      \"targets\": {\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"outputs\": [\"{options.outputFile}\"],\n          \"options\": {\n            \"lintFilePatterns\": [\"tests/game-simulation/**/*.ts\"]\n          }\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/tests/game-simulation\"],\n          \"options\": {\n            \"jestConfig\": \"tests/game-simulation/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    },\n    \"web-backend\": {\n      \"root\": \"apps/web/backend\",\n      \"sourceRoot\": \"apps/web/backend/src\",\n      \"projectType\": \"application\",\n      \"targets\": {\n        \"build\": {\n          \"executor\": \"@nrwl/node:webpack\",\n          \"outputs\": [\"{options.outputPath}\"],\n          \"options\": {\n            \"outputPath\": \"dist/apps/web/backend\",\n            \"main\": \"apps/web/backend/src/main.ts\",\n            \"tsConfig\": \"apps/web/backend/tsconfig.app.json\",\n            \"assets\": [\"apps/web/backend/src/assets\"]\n          },\n          \"configurations\": {\n            \"production\": {\n              \"optimization\": true,\n              \"extractLicenses\": true,\n              \"inspect\": false,\n              \"fileReplacements\": [\n                {\n                  \"replace\": \"apps/desktop/backend/src/environments/environment.ts\",\n                  \"with\": \"apps/desktop/backend/src/environments/environment.prod.ts\"\n                }\n              ]\n            }\n          }\n        },\n        \"serve\": {\n          \"executor\": \"@nrwl/node:node\",\n          \"options\": {\n            \"buildTarget\": \"web-backend:build\"\n          }\n        },\n        \"lint\": {\n          \"executor\": \"@nrwl/linter:eslint\",\n          \"options\": {\n            \"lintFilePatterns\": [\"apps/web/backend/**/*.ts\"]\n          },\n          \"outputs\": [\"{options.outputFile}\"]\n        },\n        \"test\": {\n          \"executor\": \"@nrwl/jest:jest\",\n          \"outputs\": [\"coverage/apps/web/backend\"],\n          \"options\": {\n            \"jestConfig\": \"apps/web/backend/jest.config.ts\",\n            \"passWithNoTests\": true\n          }\n        }\n      },\n      \"tags\": []\n    }\n  }\n}\n"
  }
]