Compare commits
No commits in common. "splashcat-exporter" and "main" have entirely different histories.
splashcat-
...
main
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8.11.0
|
||||
version: 7.29.1
|
||||
|
||||
- name: Sync node version and setup cache
|
||||
uses: actions/setup-node@v3
|
||||
|
|
|
|||
50
CHANGELOG.md
50
CHANGELOG.md
|
|
@ -1,53 +1,3 @@
|
|||
## 0.4.12
|
||||
|
||||
feat: add 6.0.0 special
|
||||
|
||||
## 0.4.11
|
||||
|
||||
chore: update `WEB_VIEW_VERSION` and queries
|
||||
|
||||
## 0.4.10
|
||||
|
||||
feat: support random primary ability
|
||||
|
||||
## 0.4.9
|
||||
|
||||
feat: add species and crown_type
|
||||
|
||||
## 0.4.8
|
||||
|
||||
chore: update `WEB_VIEW_VERSION` and queries
|
||||
|
||||
feat: update VersionData
|
||||
|
||||
## 0.4.7
|
||||
|
||||
chore: update `WEB_VIEW_VERSION`
|
||||
|
||||
## 0.4.6
|
||||
|
||||
chore: update constants
|
||||
|
||||
fix: skip updateState if history if empty
|
||||
([#81](https://github.com/spacemeowx2/s3si.ts/issues/81))
|
||||
|
||||
## 0.4.5
|
||||
|
||||
fix: list method is not auto
|
||||
|
||||
## 0.4.4
|
||||
|
||||
feat: send Anarchy (Open) Power
|
||||
|
||||
## 0.4.3
|
||||
|
||||
feat: add `list-method` option
|
||||
([#73](https://github.com/spacemeowx2/s3si.ts/issues/73))
|
||||
|
||||
## 0.4.2
|
||||
|
||||
fix: `coral_user_id` is string
|
||||
|
||||
## 0.4.1
|
||||
|
||||
feat: add support for Challenges
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -3,7 +3,7 @@
|
|||
[](https://github.com/spacemeowx2/s3si.ts/actions/workflows/ci.yaml)
|
||||
[](https://github.com/spacemeowx2/s3si.ts/actions/workflows/constant-check.yaml)
|
||||
|
||||
Export your battles from SplatNet to stat.ink and Splashcat.
|
||||
Export your battles from SplatNet to stat.ink.
|
||||
|
||||
If you have used s3s, please see [here](#migrate-from-s3s).
|
||||
|
||||
|
|
@ -17,19 +17,15 @@ If you have used s3s, please see [here](#migrate-from-s3s).
|
|||
```
|
||||
Options:
|
||||
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
||||
--exporter <exporter>, -e Exporter list to use (default: stat.ink,splashcat)
|
||||
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
|
||||
Multiple exporters can be separated by commas
|
||||
(e.g. "stat.ink,file,splashcat")
|
||||
--list-method When set to "latest", the latest 50 matches will be obtained.
|
||||
When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches).
|
||||
When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes.
|
||||
"auto" is the default setting.
|
||||
(e.g. "stat.ink,file")
|
||||
--no-progress, -n Disable progress bar
|
||||
--monitor, -m Monitor mode
|
||||
--skip-mode <mode>, -s Skip mode (default: null)
|
||||
("vs", "coop")
|
||||
--with-summary Include summary in the output
|
||||
--help Show this help message and exit
|
||||
--help Show this help message and exit`,
|
||||
```
|
||||
|
||||
3. If it's your first time running this, follow the instructions to login to
|
||||
|
|
@ -38,10 +34,6 @@ Options:
|
|||
- If you want to use a different profile, use `-p` to specify the path to the
|
||||
profile file.
|
||||
|
||||
### Splashcat Notes
|
||||
|
||||
Splashcat requires battles uploaded to use `en-US` (set with `userLang`). Splashcat will localize battle results into the user's language when displayed.
|
||||
|
||||
### Track your rank
|
||||
|
||||
- Run
|
||||
|
|
@ -75,8 +67,7 @@ Splashcat requires battles uploaded to use `en-US` (set with `userLang`). Splash
|
|||
// userLang will effect the language of the exported games to stat.ink
|
||||
"userLang": "zh-CN",
|
||||
"userCountry": "JP",
|
||||
"statInkApiKey": "...",
|
||||
"splashcatApiKey": "..."
|
||||
"statInkApiKey": "..."
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -100,4 +91,3 @@ Then run `s3si.ts`, and it will work without login prompt.
|
|||
|
||||
- https://github.com/frozenpandaman/s3s
|
||||
- https://github.com/fetus-hina/stat.ink
|
||||
- forked from https://github.com/spacemeowx2/s3si.ts
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
"https://deno.land/std@0.160.0/bytes/mod.ts": "b2e342fd3669176a27a4e15061e9d588b89c1aaf5008ab71766e23669565d179",
|
||||
"https://deno.land/std@0.160.0/encoding/base64.ts": "c57868ca7fa2fbe919f57f88a623ad34e3d970d675bdc1ff3a9d02bba7409db2",
|
||||
"https://deno.land/std@0.160.0/flags/mod.ts": "686b6b36e14b00f11c9e26cecf439021158436a6e34f60eeb0d927f0b169ae20",
|
||||
"https://deno.land/std@0.160.0/fmt/colors.ts": "9e36a716611dcd2e4865adea9c4bec916b5c60caad4cdcdc630d4974e6bb8bd4",
|
||||
"https://deno.land/std@0.160.0/io/buffer.ts": "fae02290f52301c4e0188670e730cd902f9307fb732d79c4aa14ebdc82497289",
|
||||
"https://deno.land/std@0.160.0/io/mod.ts": "6e781ebafd5cdccf9ab4afa1f499b08c513602d023cb08ceebc58758501f78bd",
|
||||
"https://deno.land/std@0.160.0/io/readers.ts": "45847ad404afd2f605eae1cff193f223462bc55eeb9ae313c2f3db28aada0fd6",
|
||||
|
|
@ -33,9 +32,6 @@
|
|||
"https://deno.land/std@0.160.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
|
||||
"https://deno.land/std@0.160.0/path/win32.ts": "ee8826dce087d31c5c81cd414714e677eb68febc40308de87a2ce4b40e10fb8d",
|
||||
"https://deno.land/std@0.160.0/streams/conversion.ts": "328afbedee0a7e0c330ac4c7b4c1af569ee53974f970230f6a78f545b93abb9b",
|
||||
"https://deno.land/std@0.160.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c",
|
||||
"https://deno.land/std@0.160.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832",
|
||||
"https://deno.land/std@0.160.0/testing/asserts.ts": "1e340c589853e82e0807629ba31a43c84ebdcdeca910c4a9705715dfdb0f5ce8",
|
||||
"https://deno.land/std@0.160.0/uuid/_common.ts": "76e1fdfb03aecf733f7b3a5edc900f5734f2433b359fdb1535f8de72873bdb3f",
|
||||
"https://deno.land/std@0.160.0/uuid/mod.ts": "e57ba10200d75f2b17570f13eba19faa6734b1be2da5091e2c01039df41274a5",
|
||||
"https://deno.land/std@0.160.0/uuid/v1.ts": "7123410ef9ce980a4f2e54a586ccde5ed7063f6f119a70d86eebd92f8e100295",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# s3si.ts GUI
|
||||
# Tauri + React + Typescript
|
||||
|
||||
## Development
|
||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||
|
||||
```
|
||||
pnpm tauri dev
|
||||
```
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||
|
|
|
|||
|
|
@ -11,32 +11,31 @@
|
|||
"lint": "eslint --max-warnings=0 src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.4.0",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"classnames": "^2.3.2",
|
||||
"daisyui": "^3.1.7",
|
||||
"i18next": "^23.2.6",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"daisyui": "^2.52.0",
|
||||
"i18next": "^22.5.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^13.0.1",
|
||||
"react-icons": "^4.10.1",
|
||||
"react-router-dom": "^6.14.1",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-icons": "^4.9.0",
|
||||
"react-router-dom": "^6.11.2",
|
||||
"react-use": "^17.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^1.4.0",
|
||||
"@types/node": "^20.3.3",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@types/node": "^20.2.5",
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@typescript-eslint/parser": "^5.61.0",
|
||||
"@vitejs/plugin-react": "^4.0.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"i18next-http-backend": "^2.2.1",
|
||||
"postcss": "^8.4.24",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-tsconfig-paths": "^4.2.0"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -10,21 +10,14 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.4.0", features = [] }
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.4.1", features = [
|
||||
"fs-all",
|
||||
"path-all",
|
||||
"process-relaunch",
|
||||
"shell-execute",
|
||||
"shell-open",
|
||||
"shell-sidecar",
|
||||
"window-all",
|
||||
] }
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
serde_json = "1.0.97"
|
||||
tokio = { version = "1.28.2", features = ["time"] }
|
||||
tauri = { version = "1.2", features = ["fs-all", "path-all", "process-relaunch", "shell-execute", "shell-open", "shell-sidecar", "window-all"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.0", features = ["time"] }
|
||||
urlencoding = "2.1.2"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ function onSelectUserClick(e) {
|
|||
}
|
||||
e.preventDefault();
|
||||
|
||||
// a little official way...
|
||||
window.__TAURI_INVOKE__({
|
||||
// very hacky way...
|
||||
window.ipc.postMessage(JSON.stringify({
|
||||
"cmd":"tauri",
|
||||
"callback":0,
|
||||
"error":0,
|
||||
"__tauriModule":"Event",
|
||||
"cmd": "tauri",
|
||||
"message":{"cmd":"emit","event":"login","payload":{"url":element.href}}
|
||||
})
|
||||
}))
|
||||
}
|
||||
function detectAndInject() {
|
||||
const element = document.getElementById('authorize-switch-approval-link');
|
||||
|
|
@ -72,13 +74,18 @@ document.addEventListener("DOMContentLoaded", () => {{
|
|||
|
||||
#[tauri::command]
|
||||
async fn open_login_window(app: tauri::AppHandle, url: String) -> Option<String> {
|
||||
let window = WindowBuilder::new(&app, "login", tauri::WindowUrl::App(url.into()))
|
||||
.title("Login")
|
||||
.center()
|
||||
.inner_size(1040.0, 960.0)
|
||||
.initialization_script(INIT_SCRIPT)
|
||||
.build()
|
||||
.ok()?;
|
||||
let encoded = urlencoding::encode(&url);
|
||||
let window = WindowBuilder::new(
|
||||
&app,
|
||||
"login",
|
||||
tauri::WindowUrl::App(format!("/redirect?url={encoded}").into()),
|
||||
)
|
||||
.title("Login")
|
||||
.center()
|
||||
.inner_size(1040.0, 960.0)
|
||||
.initialization_script(INIT_SCRIPT)
|
||||
.build()
|
||||
.ok()?;
|
||||
let result: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
|
||||
let r2 = result.clone();
|
||||
let r3 = result.clone();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"$schema": "https://github.com/tauri-apps/tauri/raw/tauri-v1.4.1/core/tauri-config-schema/schema.json",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
|
|
@ -9,7 +8,7 @@
|
|||
},
|
||||
"package": {
|
||||
"productName": "s3si-ts",
|
||||
"version": "0.4.12"
|
||||
"version": "0.4.1"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
|
|
@ -70,16 +69,7 @@
|
|||
]
|
||||
},
|
||||
"security": {
|
||||
"csp": null,
|
||||
"dangerousRemoteDomainIpcAccess": [
|
||||
{
|
||||
"windows": [
|
||||
"login"
|
||||
],
|
||||
"domain": "accounts.nintendo.com",
|
||||
"enableTauriAPI": true
|
||||
}
|
||||
]
|
||||
"csp": null
|
||||
},
|
||||
"updater": {
|
||||
"active": false,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Layout } from "components/Layout";
|
|||
import { Home } from "pages/Home";
|
||||
import { Settings } from "pages/Settings";
|
||||
import { Guide } from 'pages/Guide';
|
||||
import { RedirectLogin } from 'pages/RedirectLogin';
|
||||
import { useShowWindow } from 'hooks/useShowWindow';
|
||||
|
||||
function App() {
|
||||
|
|
@ -14,6 +15,7 @@ function App() {
|
|||
<Route index element={<Home />} />
|
||||
<Route path='/settings' element={<Settings />} />
|
||||
<Route path='/guide' element={<Guide />} />
|
||||
<Route path='/redirect' element={<RedirectLogin />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ export const OpenSplatnet: React.FC<OpenSplatnetProps> = ({ children }) => {
|
|||
</>
|
||||
}
|
||||
|
||||
const btnLoading = loading || doing;
|
||||
return <>
|
||||
<button className={classNames('btn w-full', {
|
||||
<button className={classNames('btn', {
|
||||
'btn-disabled': !result?.profile.state.loginState?.sessionToken,
|
||||
})} onClick={onClick} disabled={btnLoading}>{btnLoading ? <span className='loading' /> : children}</button>
|
||||
'loading': loading || doing,
|
||||
})} onClick={onClick}>{children}</button>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,11 +61,11 @@ export const RunPanel: React.FC<RunPanelProps> = () => {
|
|||
<Checkbox disabled={disabled || loading} value={exportCoop} onChange={setExportCoop}>{t('导出打工数据')}</Checkbox>
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={classNames('btn btn-primary w-full', {
|
||||
className={classNames('btn w-full', {
|
||||
'btn-disabled': disabled || (!exportBattle && !exportCoop),
|
||||
'loading': loading,
|
||||
})}
|
||||
disabled={loading}
|
||||
>{loading ? <span className='loading' /> : t('导出')}</button>
|
||||
>{t('导出')}</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,40 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
}
|
||||
|
||||
/* custom classes */
|
||||
|
||||
.flex-auto-all > * {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const Home: React.FC = () => {
|
|||
<Link to='/settings' className='btn'>{t('设置')}</Link>
|
||||
<div className='flex gap-2 flex-auto-all'>
|
||||
<OpenSplatnet>{t('打开鱿鱼圈3')}</OpenSplatnet>
|
||||
<a className='btn w-full' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
||||
<a className='btn' href={STAT_INK} target='_blank' rel='noreferrer'>{t('前往 stat.ink')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import { Loading } from 'components/Loading';
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
export const RedirectLogin: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const state = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const search = state.search ?? '';
|
||||
|
||||
const index = search.indexOf('url=');
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
const url = decodeURIComponent(search.substring(index + 4));
|
||||
|
||||
window.location.href = url;
|
||||
}, [state])
|
||||
|
||||
return <div className='h-full flex justify-center items-center'>
|
||||
<span className='flex justify-center items-center gap-1'><Loading className='align-middle' />{t('正在跳转到登录页面...')}</span>
|
||||
</div>
|
||||
}
|
||||
9
s3si.ts
9
s3si.ts
|
|
@ -4,7 +4,7 @@ import { flags } from "./deps.ts";
|
|||
|
||||
const parseArgs = (args: string[]) => {
|
||||
const parsed = flags.parse(args, {
|
||||
string: ["profilePath", "exporter", "skipMode", "listMethod"],
|
||||
string: ["profilePath", "exporter", "skipMode"],
|
||||
boolean: ["help", "noProgress", "monitor", "withSummary"],
|
||||
alias: {
|
||||
"help": "h",
|
||||
|
|
@ -15,7 +15,6 @@ const parseArgs = (args: string[]) => {
|
|||
"skipMode": ["s", "skip-mode"],
|
||||
"withSummary": "with-summary",
|
||||
"withStages": "with-stages",
|
||||
"listMethod": "list-method",
|
||||
},
|
||||
});
|
||||
return parsed;
|
||||
|
|
@ -30,11 +29,7 @@ Options:
|
|||
--profile-path <path>, -p Path to config file (default: ./profile.json)
|
||||
--exporter <exporter>, -e Exporter list to use (default: stat.ink)
|
||||
Multiple exporters can be separated by commas
|
||||
(e.g. "stat.ink,file,mongodb,splashcat")
|
||||
--list-method When set to "latest", the latest 50 matches will be obtained.
|
||||
When set to "all", matches of all modes will be obtained with a maximum of 250 matches (5 modes x 50 matches).
|
||||
When set to "auto", the latest 50 matches will be obtained. If 50 matches have not been uploaded yet, matches will be obtained from the list of all modes.
|
||||
"auto" is the default setting.
|
||||
(e.g. "stat.ink,file,mongodb")
|
||||
--no-progress, -n Disable progress bar
|
||||
--monitor, -m Monitor mode
|
||||
--skip-mode <mode>, -s Skip mode (default: null)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
},
|
||||
"npm": {
|
||||
"specifiers": {
|
||||
"mongodb": "mongodb@5.5.0",
|
||||
"mongodb": "mongodb@5.1.0",
|
||||
"splatnet3-types": "splatnet3-types@0.2.20230227204004"
|
||||
},
|
||||
"packages": {
|
||||
|
|
@ -110,10 +110,6 @@
|
|||
"integrity": "sha512-y09gBGusgHtinMon/GVbv1J6FrXhnr/+6hqLlSmEFzkz6PodqF6TxjyvfvY3AfO+oG1mgUtbC86xSbOlwvM62Q==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"bson@5.3.0": {
|
||||
"integrity": "sha512-ukmCZMneMlaC5ebPHXIkP8YJzNl5DC41N5MAIvKDqLggdao342t4McltoJBQfQya/nHBWAcSsYRqlXPoQkTJag==",
|
||||
"dependencies": {}
|
||||
},
|
||||
"ip@2.0.0": {
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||
"dependencies": {}
|
||||
|
|
@ -138,15 +134,6 @@
|
|||
"socks": "socks@2.7.1"
|
||||
}
|
||||
},
|
||||
"mongodb@5.5.0": {
|
||||
"integrity": "sha512-XgrkUgAAdfnZKQfk5AsYL8j7O99WHd4YXPxYxnh8dZxD+ekYWFRA3JktUsBnfg+455Smf75/+asoU/YLwNGoQQ==",
|
||||
"dependencies": {
|
||||
"bson": "bson@5.3.0",
|
||||
"mongodb-connection-string-url": "mongodb-connection-string-url@2.6.0",
|
||||
"saslprep": "saslprep@1.0.3",
|
||||
"socks": "socks@2.7.1"
|
||||
}
|
||||
},
|
||||
"punycode@2.3.0": {
|
||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||
"dependencies": {}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { MongoDB } from "../deps.ts";
|
||||
import { DEFAULT_ENV } from "../src/env.ts";
|
||||
import { MongoDBExporter } from "../src/exporters/mongodb.ts";
|
||||
import { SplashcatExporter } from "../src/exporters/splashcat.ts";
|
||||
import { FileStateBackend, Profile } from "../src/state.ts";
|
||||
|
||||
const env = DEFAULT_ENV;
|
||||
|
|
@ -14,12 +13,6 @@ if (!profile.state.mongoDbUri) {
|
|||
Deno.exit(1);
|
||||
}
|
||||
|
||||
const splashcatClient = new SplashcatExporter({
|
||||
env,
|
||||
splashcatApiKey: profile.state.splashcatApiKey!,
|
||||
uploadMode: "manual",
|
||||
});
|
||||
|
||||
const mongoDbClient = new MongoDB.MongoClient(profile.state.mongoDbUri);
|
||||
const battlesCollection = mongoDbClient.db("splashcat").collection("battles");
|
||||
|
||||
|
|
@ -34,29 +27,31 @@ let count = 0;
|
|||
const erroredBattles = [];
|
||||
|
||||
for await (const doc of cursor) {
|
||||
const { data, splatNetData, _id } = doc;
|
||||
const { splatNetData, _id } = doc;
|
||||
|
||||
// start time for performance tracking, needs to be very accurate
|
||||
const startTime = new Date();
|
||||
|
||||
try {
|
||||
if (data) {
|
||||
await splashcatClient.exportGame(data);
|
||||
} else {
|
||||
await splashcatClient.exportGame({
|
||||
type: "VsInfo",
|
||||
detail: splatNetData,
|
||||
bankaraMatchChallenge: null,
|
||||
challengeProgress: null,
|
||||
groupInfo: null,
|
||||
listNode: null,
|
||||
rankBeforeState: null,
|
||||
rankState: null,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Failed to export game", e);
|
||||
erroredBattles.push(e.toString());
|
||||
splatNetData.playedTime = splatNetData.playedTime.toISOString();
|
||||
|
||||
const response = await fetch("http://127.0.0.1:8000/battles/api/upload/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${profile.state.splashcatApiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"data_type": "splatnet3",
|
||||
"battle": splatNetData,
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to upload ${splatNetData.id}`);
|
||||
erroredBattles.push({
|
||||
id: doc.gameId,
|
||||
error: await response.text(),
|
||||
});
|
||||
}
|
||||
|
||||
// end time for performance tracking, needs to be very accurate
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
HistoryGroups,
|
||||
RankParam,
|
||||
} from "./types.ts";
|
||||
import { battleTime, gameId } from "./utils.ts";
|
||||
import { gameId, parseHistoryDetailId } from "./utils.ts";
|
||||
import { getSeason } from "./VersionData.ts";
|
||||
|
||||
const splusParams = () => {
|
||||
|
|
@ -193,6 +193,17 @@ function addRank(
|
|||
};
|
||||
}
|
||||
|
||||
const battleTime = (id: string) => {
|
||||
const { timestamp } = parseHistoryDetailId(id);
|
||||
|
||||
const dateStr = timestamp.replace(
|
||||
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
|
||||
"$1-$2-$3T$4:$5:$6Z",
|
||||
);
|
||||
|
||||
return new Date(dateStr);
|
||||
};
|
||||
|
||||
type FlattenItem = {
|
||||
id: string;
|
||||
gameId: string;
|
||||
|
|
@ -347,10 +358,6 @@ export class RankTracker {
|
|||
async updateState(
|
||||
history: HistoryGroups<BattleListNode>["nodes"],
|
||||
) {
|
||||
if (history.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// history order by time. 0 is the oldest.
|
||||
const flatten: FlattenItem[] = await Promise.all(
|
||||
history
|
||||
|
|
|
|||
|
|
@ -30,18 +30,6 @@ export const SEASONS: Season[] = [
|
|||
start: new Date("2023-06-01T00:00:00+00:00"),
|
||||
end: new Date("2023-09-01T00:00:00+00:00"),
|
||||
},
|
||||
{
|
||||
id: "season202309",
|
||||
name: "Drizzle Season 2023",
|
||||
start: new Date("2023-09-01T00:00:00+00:00"),
|
||||
end: new Date("2023-12-01T00:00:00+00:00"),
|
||||
},
|
||||
{
|
||||
id: "season202312",
|
||||
name: "Chill Season 2023",
|
||||
start: new Date("2023-12-01T00:00:00+00:00"),
|
||||
end: new Date("2024-03-01T00:00:00+00:00"),
|
||||
},
|
||||
];
|
||||
|
||||
export const getSeason = (date: Date): Season | undefined => {
|
||||
|
|
|
|||
162
src/app.ts
162
src/app.ts
|
|
@ -1,8 +1,8 @@
|
|||
import { loginManually } from "./iksm.ts";
|
||||
import { MultiProgressBar, Mutex } from "../deps.ts";
|
||||
import { MultiProgressBar } from "../deps.ts";
|
||||
import { FileStateBackend, Profile, StateBackend } from "./state.ts";
|
||||
import { Splatnet3 } from "./splatnet3.ts";
|
||||
import { BattleListType, Game, GameExporter, ListMethod } from "./types.ts";
|
||||
import { BattleListType, Game, GameExporter } from "./types.ts";
|
||||
import { Cache, FileCache } from "./cache.ts";
|
||||
import { StatInkExporter } from "./exporters/stat.ink.ts";
|
||||
import { FileExporter } from "./exporters/file.ts";
|
||||
|
|
@ -10,7 +10,6 @@ import { delay, showError } from "./utils.ts";
|
|||
import { GameFetcher } from "./GameFetcher.ts";
|
||||
import { DEFAULT_ENV, Env } from "./env.ts";
|
||||
import { MongoDBExporter } from "./exporters/mongodb.ts";
|
||||
import { SplashcatExporter } from "./exporters/splashcat.ts";
|
||||
|
||||
export type Opts = {
|
||||
profilePath: string;
|
||||
|
|
@ -20,7 +19,6 @@ export type Opts = {
|
|||
withSummary: boolean;
|
||||
withStages: boolean;
|
||||
skipMode?: string;
|
||||
listMethod?: string;
|
||||
cache?: Cache;
|
||||
stateBackend?: StateBackend;
|
||||
env: Env;
|
||||
|
|
@ -28,12 +26,11 @@ export type Opts = {
|
|||
|
||||
export const DEFAULT_OPTS: Opts = {
|
||||
profilePath: "./profile.json",
|
||||
exporter: "stat.ink,splashcat",
|
||||
exporter: "stat.ink",
|
||||
noProgress: false,
|
||||
monitor: false,
|
||||
withSummary: false,
|
||||
withStages: true,
|
||||
listMethod: "auto",
|
||||
env: DEFAULT_ENV,
|
||||
};
|
||||
|
||||
|
|
@ -58,103 +55,6 @@ class StepProgress {
|
|||
}
|
||||
}
|
||||
|
||||
interface GameListFetcher {
|
||||
/**
|
||||
* Return not exported game list.
|
||||
* [0] is the latest game.
|
||||
* @param exporter GameExporter
|
||||
*/
|
||||
fetch(exporter: GameExporter): Promise<string[]>;
|
||||
}
|
||||
|
||||
class BattleListFetcher implements GameListFetcher {
|
||||
protected listMethod: ListMethod;
|
||||
protected allBattleList?: string[];
|
||||
protected latestBattleList?: string[];
|
||||
protected allLock = new Mutex();
|
||||
protected latestLock = new Mutex();
|
||||
|
||||
constructor(
|
||||
listMethod: string,
|
||||
protected splatnet: Splatnet3,
|
||||
) {
|
||||
if (listMethod === "all") {
|
||||
this.listMethod = "all";
|
||||
} else if (listMethod === "latest") {
|
||||
this.listMethod = "latest";
|
||||
} else {
|
||||
this.listMethod = "auto";
|
||||
}
|
||||
}
|
||||
|
||||
protected getAllBattleList() {
|
||||
return this.allLock.use(async () => {
|
||||
if (!this.allBattleList) {
|
||||
this.allBattleList = await this.splatnet.getAllBattleList();
|
||||
}
|
||||
return this.allBattleList;
|
||||
});
|
||||
}
|
||||
|
||||
protected getLatestBattleList() {
|
||||
return this.latestLock.use(async () => {
|
||||
if (!this.latestBattleList) {
|
||||
this.latestBattleList = await this.splatnet.getBattleList();
|
||||
}
|
||||
return this.latestBattleList;
|
||||
});
|
||||
}
|
||||
|
||||
private async innerFetch(exporter: GameExporter) {
|
||||
if (this.listMethod === "latest") {
|
||||
return await exporter.notExported({
|
||||
type: "VsInfo",
|
||||
list: await this.getLatestBattleList(),
|
||||
});
|
||||
}
|
||||
if (this.listMethod === "all") {
|
||||
return await exporter.notExported({
|
||||
type: "VsInfo",
|
||||
list: await this.getAllBattleList(),
|
||||
});
|
||||
}
|
||||
if (this.listMethod === "auto") {
|
||||
const latestList = await exporter.notExported({
|
||||
type: "VsInfo",
|
||||
list: await this.getLatestBattleList(),
|
||||
});
|
||||
if (latestList.length === 50) {
|
||||
return await exporter.notExported({
|
||||
type: "VsInfo",
|
||||
list: await this.getAllBattleList(),
|
||||
});
|
||||
}
|
||||
return latestList;
|
||||
}
|
||||
|
||||
throw new TypeError(`Unknown listMethod: ${this.listMethod}`);
|
||||
}
|
||||
|
||||
async fetch(exporter: GameExporter) {
|
||||
return [...await this.innerFetch(exporter)].reverse();
|
||||
}
|
||||
}
|
||||
|
||||
class CoopListFetcher implements GameListFetcher {
|
||||
constructor(
|
||||
protected splatnet: Splatnet3,
|
||||
) {}
|
||||
|
||||
async fetch(exporter: GameExporter) {
|
||||
return [
|
||||
...await exporter.notExported({
|
||||
type: "CoopInfo",
|
||||
list: await this.splatnet.getBattleList(BattleListType.Coop),
|
||||
}),
|
||||
].reverse();
|
||||
}
|
||||
}
|
||||
|
||||
function progress({ total, currentUrl, done }: StepProgress): Progress {
|
||||
return {
|
||||
total,
|
||||
|
|
@ -175,12 +75,6 @@ export class App {
|
|||
env: opts.env,
|
||||
});
|
||||
this.env = opts.env;
|
||||
|
||||
if (
|
||||
opts.listMethod && !["all", "auto", "latest"].includes(opts.listMethod)
|
||||
) {
|
||||
throw new TypeError(`Unknown listMethod: ${opts.listMethod}`);
|
||||
}
|
||||
}
|
||||
|
||||
getSkipMode(): ("vs" | "coop")[] {
|
||||
|
|
@ -243,27 +137,6 @@ export class App {
|
|||
);
|
||||
}
|
||||
|
||||
if (exporters.includes("splashcat")) {
|
||||
if (!state.splashcatApiKey) {
|
||||
const key = (await this.env.prompts.prompt(
|
||||
"Splashcat API key is not set. Please enter below.",
|
||||
)).trim();
|
||||
if (!key) {
|
||||
this.env.logger.error("API key is required.");
|
||||
Deno.exit(1);
|
||||
}
|
||||
await this.profile.writeState({
|
||||
...state,
|
||||
splashcatApiKey: key,
|
||||
});
|
||||
}
|
||||
out.push(new SplashcatExporter({
|
||||
env: this.env,
|
||||
uploadMode: this.opts.monitor ? "Monitoring" : "Manual",
|
||||
splashcatApiKey: this.profile.state.splashcatApiKey!,
|
||||
}));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
exporterProgress(title: string) {
|
||||
|
|
@ -311,10 +184,8 @@ export class App {
|
|||
if (skipMode.includes("vs") || exporters.length === 0) {
|
||||
this.env.logger.log("Skip exporting VS games.");
|
||||
} else {
|
||||
const gameListFetcher = new BattleListFetcher(
|
||||
this.opts.listMethod ?? "auto",
|
||||
splatnet,
|
||||
);
|
||||
this.env.logger.log("Fetching battle list...");
|
||||
const gameList = await splatnet.getBattleList();
|
||||
|
||||
const { redraw, endBar } = this.exporterProgress("Export vs games");
|
||||
const fetcher = new GameFetcher({
|
||||
|
|
@ -333,7 +204,7 @@ export class App {
|
|||
type: "VsInfo",
|
||||
fetcher,
|
||||
exporter: e,
|
||||
gameListFetcher,
|
||||
gameList,
|
||||
stepProgress: stats[e.name],
|
||||
onStep: () => {
|
||||
redraw(e.name, progress(stats[e.name]));
|
||||
|
|
@ -367,7 +238,10 @@ export class App {
|
|||
if (skipMode.includes("coop") || exporters.length === 0) {
|
||||
this.env.logger.log("Skip exporting coop games.");
|
||||
} else {
|
||||
const gameListFetcher = new CoopListFetcher(splatnet);
|
||||
this.env.logger.log("Fetching coop battle list...");
|
||||
const coopBattleList = await splatnet.getBattleList(
|
||||
BattleListType.Coop,
|
||||
);
|
||||
|
||||
const { redraw, endBar } = this.exporterProgress("Export coop games");
|
||||
const fetcher = new GameFetcher({
|
||||
|
|
@ -384,7 +258,7 @@ export class App {
|
|||
type: "CoopInfo",
|
||||
fetcher,
|
||||
exporter: e,
|
||||
gameListFetcher,
|
||||
gameList: coopBattleList,
|
||||
stepProgress: stats[e.name],
|
||||
onStep: () => {
|
||||
redraw(e.name, progress(stats[e.name]));
|
||||
|
|
@ -518,24 +392,30 @@ export class App {
|
|||
* @param gameList ID list of games, sorted by date, newest first
|
||||
* @param onStep Callback function called when a game is exported
|
||||
*/
|
||||
private async exportGameList({
|
||||
async exportGameList({
|
||||
type,
|
||||
fetcher,
|
||||
exporter,
|
||||
gameListFetcher,
|
||||
gameList,
|
||||
stepProgress,
|
||||
onStep,
|
||||
}: {
|
||||
type: Game["type"];
|
||||
exporter: GameExporter;
|
||||
fetcher: GameFetcher;
|
||||
gameListFetcher: GameListFetcher;
|
||||
gameList: string[];
|
||||
stepProgress: StepProgress;
|
||||
onStep: () => void;
|
||||
}): Promise<StepProgress> {
|
||||
onStep?.();
|
||||
|
||||
const workQueue = await gameListFetcher.fetch(exporter);
|
||||
const workQueue = [
|
||||
...await exporter.notExported({
|
||||
type,
|
||||
list: gameList,
|
||||
}),
|
||||
]
|
||||
.reverse();
|
||||
|
||||
const step = async (id: string) => {
|
||||
const detail = await fetcher.fetch(type, id);
|
||||
|
|
|
|||
|
|
@ -2,41 +2,26 @@ import type { StatInkPostBody, VsHistoryDetail } from "./types.ts";
|
|||
|
||||
export const AGENT_NAME = "splashcat / s3si.ts";
|
||||
export const AGENT_VERSION = "1.1.1";
|
||||
export const S3SI_VERSION = "0.4.12";
|
||||
export const S3SI_VERSION = "0.4.1";
|
||||
export const COMBINED_VERSION = `${AGENT_VERSION}/${S3SI_VERSION}`;
|
||||
export const NSOAPP_VERSION = "2.8.1";
|
||||
export const WEB_VIEW_VERSION = "6.0.0-e135295b";
|
||||
export const NSOAPP_VERSION = "2.5.1";
|
||||
export const WEB_VIEW_VERSION = "4.0.0-d5178440";
|
||||
export enum Queries {
|
||||
HomeQuery =
|
||||
"51fc56bbf006caf37728914aa8bc0e2c86a80cf195b4d4027d6822a3623098a8",
|
||||
LatestBattleHistoriesQuery =
|
||||
"b24d22fd6cb251c515c2b90044039698aa27bc1fab15801d83014d919cd45780",
|
||||
RegularBattleHistoriesQuery =
|
||||
"2fe6ea7a2de1d6a888b7bd3dbeb6acc8e3246f055ca39b80c4531bbcd0727bba",
|
||||
BankaraBattleHistoriesQuery =
|
||||
"9863ea4744730743268e2940396e21b891104ed40e2286789f05100b45a0b0fd",
|
||||
XBattleHistoriesQuery =
|
||||
"eb5996a12705c2e94813a62e05c0dc419aad2811b8d49d53e5732290105559cb",
|
||||
EventBattleHistoriesQuery =
|
||||
"e47f9aac5599f75c842335ef0ab8f4c640e8bf2afe588a3b1d4b480ee79198ac",
|
||||
PrivateBattleHistoriesQuery =
|
||||
"fef94f39b9eeac6b2fac4de43bc0442c16a9f2df95f4d367dd8a79d7c5ed5ce7",
|
||||
VsHistoryDetailQuery =
|
||||
"f893e1ddcfb8a4fd645fd75ced173f18b2750e5cfba41d2669b9814f6ceaec46",
|
||||
CoopHistoryQuery =
|
||||
"0f8c33970a425683bb1bdecca50a0ca4fb3c3641c0b2a1237aedfde9c0cb2b8f",
|
||||
CoopHistoryDetailQuery =
|
||||
"42262d241291d7324649e21413b29da88c0314387d8fdf5f6637a2d9d29954ae",
|
||||
HomeQuery = "7dcc64ea27a08e70919893a0d3f70871",
|
||||
LatestBattleHistoriesQuery = "0d90c7576f1916469b2ae69f64292c02",
|
||||
RegularBattleHistoriesQuery = "3baef04b095ad8975ea679d722bc17de",
|
||||
BankaraBattleHistoriesQuery = "0438ea6978ae8bd77c5d1250f4f84803",
|
||||
XBattleHistoriesQuery = "6796e3cd5dc3ebd51864dc709d899fc5",
|
||||
PrivateBattleHistoriesQuery = "8e5ae78b194264a6c230e262d069bd28",
|
||||
VsHistoryDetailQuery = "9ee0099fbe3d8db2a838a75cf42856dd",
|
||||
CoopHistoryQuery = "91b917becd2fa415890f5b47e15ffb15",
|
||||
CoopHistoryDetailQuery = "379f0d9b78b531be53044bcac031b34b",
|
||||
myOutfitCommonDataFilteringConditionQuery =
|
||||
"ac20c44a952131cb0c9d00eda7bc1a84c1a99546f0f1fc170212d5a6bb51a426",
|
||||
myOutfitCommonDataEquipmentsQuery =
|
||||
"45a4c343d973864f7bb9e9efac404182be1d48cf2181619505e9b7cd3b56a6e8",
|
||||
HistoryRecordQuery =
|
||||
"0a62c0152f27c4218cf6c87523377521c2cff76a4ef0373f2da3300079bf0388",
|
||||
ConfigureAnalyticsQuery =
|
||||
"2a9302bdd09a13f8b344642d4ed483b9464f20889ac17401e993dfa5c2bb3607",
|
||||
StageRecordQuery =
|
||||
"c8b31c491355b4d889306a22bd9003ac68f8ce31b2d5345017cdd30a2c8056f3",
|
||||
"d02ab22c9dccc440076055c8baa0fa7a",
|
||||
myOutfitCommonDataEquipmentsQuery = "d29cd0c2b5e6bac90dd5b817914832f8",
|
||||
HistoryRecordQuery = "d9246baf077b2a29b5f7aac321810a77",
|
||||
ConfigureAnalyticsQuery = "f8ae00773cc412a50dd41a6d9a159ddd",
|
||||
StageRecordQuery = "f08a932d533845dde86e674e03bbb7d3",
|
||||
}
|
||||
export const S3SI_LINK = "https://forgejo.catgirlin.space/catgirl/s3si.ts";
|
||||
|
||||
|
|
@ -109,10 +94,6 @@ export const SPLATNET3_STATINK_MAP: {
|
|||
"sameride",
|
||||
"380e541b5bc5e49d77ff1a616f1343aeba01d500fee36aaddf8f09d74bd3d3bc":
|
||||
"tripletornado",
|
||||
"8a7ee88a06407f4be1595ef8af4d2d2ac22bbf213a622cd19bbfaf4d0f36bcd7":
|
||||
"teioika",
|
||||
"a75eac34675bc0d4bd9ca9977cf22472848f89e28e08ee986b4461a3f2af28fc":
|
||||
"ultra_chakuchi",
|
||||
},
|
||||
WATER_LEVEL_MAP: {
|
||||
0: "low",
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
/**
|
||||
* A battle to be uploaded to Splashcat. Any SplatNet 3 strings should use en-US locale.
|
||||
* Splashcat will translate strings into the user's language.
|
||||
*/
|
||||
export interface SplashcatBattle {
|
||||
anarchy?: Anarchy;
|
||||
/**
|
||||
* The en-US string for the award. Splashcat will translate this into the user's language
|
||||
* and manage the award's rank.
|
||||
*/
|
||||
awards: string[];
|
||||
challenge?: Challenge;
|
||||
duration: number;
|
||||
judgement: SplashcatBattleJudgement;
|
||||
knockout?: Knockout;
|
||||
playedTime: Date;
|
||||
splatfest?: Splatfest;
|
||||
/**
|
||||
* base64 decoded and split by `:` to get the last section
|
||||
*/
|
||||
splatnetId: string;
|
||||
teams: Team[];
|
||||
vsMode: VsMode;
|
||||
vsRule: VsRule;
|
||||
vsStageId: number;
|
||||
xBattle?: XBattle;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export interface Anarchy {
|
||||
mode?: AnarchyMode;
|
||||
pointChange?: number;
|
||||
points?: number;
|
||||
power?: number;
|
||||
rank?: Rank;
|
||||
sPlusNumber?: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export type AnarchyMode = "SERIES" | "OPEN";
|
||||
|
||||
export type Rank = "C-" | "C" | "C+" | "B-" | "B" | "B+" | "A-" | "A" | "A+" | "S" | "S+";
|
||||
|
||||
export interface Challenge {
|
||||
/**
|
||||
* base64 decoded and split by `-` to get the last section
|
||||
*/
|
||||
id?: string;
|
||||
power?: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export type SplashcatBattleJudgement = "WIN" | "LOSE" | "DRAW" | "EXEMPTED_LOSE" | "DEEMED_LOSE";
|
||||
|
||||
export type Knockout = "NEITHER" | "WIN" | "LOSE";
|
||||
|
||||
export interface Splatfest {
|
||||
cloutMultiplier?: CloutMultiplier;
|
||||
mode?: SplatfestMode;
|
||||
power?: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export type CloutMultiplier = "NONE" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
|
||||
|
||||
export type SplatfestMode = "OPEN" | "PRO";
|
||||
|
||||
export interface Team {
|
||||
color: Color;
|
||||
festStreakWinCount?: number;
|
||||
festTeamName?: string;
|
||||
festUniformBonusRate?: number;
|
||||
festUniformName?: string;
|
||||
isMyTeam: boolean;
|
||||
judgement?: TeamJudgement;
|
||||
noroshi?: number;
|
||||
order: number;
|
||||
paintRatio?: number;
|
||||
players?: Player[];
|
||||
score?: number;
|
||||
tricolorRole?: TricolorRole;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export interface Color {
|
||||
a: number;
|
||||
b: number;
|
||||
g: number;
|
||||
r: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export type TeamJudgement = "WIN" | "LOSE" | "DRAW";
|
||||
|
||||
export interface Player {
|
||||
assists?: number;
|
||||
/**
|
||||
* Array of badge IDs. Use JSON `null` for empty slots.
|
||||
*/
|
||||
badges: Array<number | null>;
|
||||
clothingGear: Gear;
|
||||
deaths?: number;
|
||||
disconnected: boolean;
|
||||
headGear: Gear;
|
||||
isMe: boolean;
|
||||
/**
|
||||
* Should report the same way that SplatNet 3 does (kills + assists)
|
||||
*/
|
||||
kills?: number;
|
||||
name: string;
|
||||
nameId?: string;
|
||||
noroshiTry?: number;
|
||||
nplnId: string;
|
||||
paint: number;
|
||||
shoesGear: Gear;
|
||||
specials?: number;
|
||||
species: Species;
|
||||
splashtagBackgroundId: number;
|
||||
title: string;
|
||||
weaponId: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* A piece of gear. Use en-US locale for name and all abilities.
|
||||
*/
|
||||
export interface Gear {
|
||||
name?: string;
|
||||
primaryAbility?: string;
|
||||
secondaryAbilities?: string[];
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export type Species = "INKLING" | "OCTOLING";
|
||||
|
||||
export type TricolorRole = "ATTACK1" | "ATTACK2" | "DEFENSE";
|
||||
|
||||
export type VsMode = "BANKARA" | "X_MATCH" | "REGULAR" | "FEST" | "PRIVATE" | "CHALLENGE";
|
||||
|
||||
export type VsRule = "AREA" | "TURF_WAR" | "TRI_COLOR" | "LOFT" | "CLAM" | "GOAL";
|
||||
|
||||
export interface XBattle {
|
||||
xPower?: number;
|
||||
xRank?: number;
|
||||
[property: string]: any;
|
||||
}
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
import {
|
||||
USERAGENT,
|
||||
} from "../constant.ts";
|
||||
import {
|
||||
Color,
|
||||
ExportResult,
|
||||
Game,
|
||||
GameExporter,
|
||||
Nameplate,
|
||||
PlayerGear,
|
||||
VsInfo,
|
||||
VsPlayer,
|
||||
VsTeam,
|
||||
} from "../types.ts";
|
||||
import { base64, msgpack, Mutex } from "../../deps.ts";
|
||||
import { APIError } from "../APIError.ts";
|
||||
import {
|
||||
parseHistoryDetailId,
|
||||
} from "../utils.ts";
|
||||
import { Env } from "../env.ts";
|
||||
import { Gear, Player, SplashcatBattle, Team, TeamJudgement } from "./splashcat-types.ts";
|
||||
|
||||
class SplashcatAPI {
|
||||
splashcatApiBase = "https://splashcat.ink";
|
||||
FETCH_LOCK = new Mutex();
|
||||
cache: Record<string, unknown> = {};
|
||||
|
||||
constructor(private splashcatApiKey: string, private env: Env) {}
|
||||
|
||||
requestHeaders() {
|
||||
return {
|
||||
"User-Agent": USERAGENT,
|
||||
"Authorization": `Bearer ${this.splashcatApiKey}`,
|
||||
};
|
||||
}
|
||||
|
||||
async uuidList(): Promise<string[]> {
|
||||
const fetch = this.env.newFetcher();
|
||||
const response = await fetch.get({
|
||||
url: `${this.splashcatApiBase}/battles/api/recent/`,
|
||||
headers: this.requestHeaders(),
|
||||
});
|
||||
|
||||
const uuidResult: Record<string, unknown> = await response.json();
|
||||
|
||||
return uuidResult.battle_ids as string[];
|
||||
}
|
||||
|
||||
async postBattle(body: unknown) {
|
||||
const fetch = this.env.newFetcher();
|
||||
const resp = await fetch.post({
|
||||
url: `${this.splashcatApiBase}/battles/api/upload/`,
|
||||
headers: {
|
||||
...this.requestHeaders(),
|
||||
"Content-Type": "application/x-msgpack",
|
||||
},
|
||||
body: msgpack.encode(body),
|
||||
});
|
||||
|
||||
const json = await resp.json();
|
||||
|
||||
if (resp.status !== 200 && resp.status !== 201) {
|
||||
throw new APIError({
|
||||
response: resp,
|
||||
message: "Failed to export battle",
|
||||
json,
|
||||
});
|
||||
}
|
||||
|
||||
if (json.error) {
|
||||
throw new APIError({
|
||||
response: resp,
|
||||
message: "Failed to export battle",
|
||||
json,
|
||||
});
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
export class SplashcatExporter implements GameExporter {
|
||||
name = "Splashcat";
|
||||
private api: SplashcatAPI;
|
||||
private uploadMode: string;
|
||||
|
||||
constructor(
|
||||
{ splashcatApiKey, uploadMode, env }: {
|
||||
splashcatApiKey: string;
|
||||
uploadMode: string;
|
||||
env: Env;
|
||||
},
|
||||
) {
|
||||
this.api = new SplashcatAPI(splashcatApiKey, env);
|
||||
this.uploadMode = uploadMode;
|
||||
}
|
||||
async exportGame(game: Game): Promise<ExportResult> {
|
||||
if (game.type === "VsInfo") {
|
||||
const body = await this.mapBattle(game);
|
||||
const resp = await this.api.postBattle(body);
|
||||
console.log(resp);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
url: undefined,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: "skip",
|
||||
reason: "Splashcat does not support Salmon Run",
|
||||
}
|
||||
}
|
||||
}
|
||||
static getGameIdOld(id: string) { // very similar to the file exporter
|
||||
const { uid, timestamp } = parseHistoryDetailId(id);
|
||||
|
||||
return `${uid}_${timestamp}Z`;
|
||||
}
|
||||
|
||||
static getGameId(id: string) {
|
||||
const plainText = new TextDecoder().decode(base64.decode(id));
|
||||
|
||||
return plainText.split(':').at(-1);
|
||||
}
|
||||
async notExported(
|
||||
{ type, list }: { list: string[]; type: Game["type"] },
|
||||
): Promise<string[]> {
|
||||
if (type !== "VsInfo") return [];
|
||||
const uuid = await this.api.uuidList();
|
||||
|
||||
const out: string[] = [];
|
||||
|
||||
for (const id of list) {
|
||||
const oldGameId = SplashcatExporter.getGameIdOld(id);
|
||||
const gameId = SplashcatExporter.getGameId(id);
|
||||
|
||||
if (
|
||||
!uuid.includes(oldGameId) && !uuid.includes(gameId)
|
||||
) {
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
mapPlayer = (
|
||||
player: VsPlayer,
|
||||
_index: number,
|
||||
): Player => {
|
||||
const result: Player = {
|
||||
badges: (player.nameplate as Nameplate).badges.map((i) => i ? Number(new TextDecoder().decode(base64.decode(i.id)).split("-")[1]) : null),
|
||||
splashtagBackgroundId: Number(new TextDecoder().decode(base64.decode((player.nameplate as Nameplate).background.id)).split('-')[1]),
|
||||
clothingGear: this.mapGear(player.clothingGear),
|
||||
headGear: this.mapGear(player.headGear),
|
||||
shoesGear: this.mapGear(player.shoesGear),
|
||||
disconnected: player.result ? false : true,
|
||||
isMe: player.isMyself,
|
||||
name: player.name,
|
||||
nameId: player.nameId ?? "",
|
||||
nplnId: new TextDecoder().decode(base64.decode(player.id)).split(":").at(-1),
|
||||
paint: player.paint,
|
||||
species: player.species,
|
||||
weaponId: Number(new TextDecoder().decode(base64.decode(player.weapon.id)).split("-")[1]),
|
||||
assists: player.result?.assist,
|
||||
deaths: player.result?.death,
|
||||
kills: player.result?.kill,
|
||||
specials: player.result?.special,
|
||||
noroshiTry: player.result?.noroshiTry ?? undefined,
|
||||
title: player.byname,
|
||||
}
|
||||
return result;
|
||||
};
|
||||
async mapBattle(
|
||||
{
|
||||
groupInfo,
|
||||
challengeProgress,
|
||||
bankaraMatchChallenge,
|
||||
listNode,
|
||||
detail: vsDetail,
|
||||
rankBeforeState,
|
||||
rankState,
|
||||
}: VsInfo,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const {
|
||||
knockout,
|
||||
vsRule: { rule },
|
||||
myTeam,
|
||||
otherTeams,
|
||||
bankaraMatch,
|
||||
festMatch,
|
||||
playedTime,
|
||||
} = vsDetail;
|
||||
|
||||
const self = vsDetail.myTeam.players.find((i) => i.isMyself);
|
||||
if (!self) {
|
||||
throw new Error("Self not found");
|
||||
}
|
||||
|
||||
if (otherTeams.length === 0) {
|
||||
throw new Error(`Other teams is empty`);
|
||||
}
|
||||
|
||||
let anarchyMode: "OPEN" | "SERIES" | undefined;
|
||||
if (vsDetail.bankaraMatch?.mode) {
|
||||
anarchyMode = vsDetail.bankaraMatch.mode === "OPEN" ? "OPEN" : "SERIES"
|
||||
}
|
||||
|
||||
const rank = rankState?.rank.substring(0, 2) ?? undefined;
|
||||
const sPlusNumber = rankState?.rank.substring(2) ?? undefined;
|
||||
|
||||
const result: SplashcatBattle = {
|
||||
splatnetId: await SplashcatExporter.getGameId(vsDetail.id),
|
||||
duration: vsDetail.duration,
|
||||
judgement: vsDetail.judgement,
|
||||
playedTime: new Date(vsDetail.playedTime).toISOString(),
|
||||
vsMode: vsDetail.vsMode.mode === "LEAGUE" ? "CHALLENGE" : vsDetail.vsMode.mode,
|
||||
vsRule: vsDetail.vsRule.rule,
|
||||
vsStageId: Number(new TextDecoder().decode(base64.decode(vsDetail.vsStage.id)).split("-")[1]),
|
||||
anarchy: vsDetail.vsMode.mode === "BANKARA" ? {
|
||||
mode: anarchyMode,
|
||||
pointChange: vsDetail.bankaraMatch?.earnedUdemaePoint ?? undefined,
|
||||
power: vsDetail.bankaraMatch?.bankaraPower?.power ?? undefined,
|
||||
points: rankState?.rankPoint ?? undefined,
|
||||
rank,
|
||||
sPlusNumber: sPlusNumber ? Number(sPlusNumber) : undefined,
|
||||
} : undefined,
|
||||
knockout: vsDetail.knockout ?? undefined,
|
||||
splatfest: vsDetail.vsMode.mode === "FEST" ? {
|
||||
cloutMultiplier: vsDetail.festMatch?.dragonMatchType === "NORMAL" ? "NONE" : (vsDetail.festMatch?.dragonMatchType ?? undefined),
|
||||
power: vsDetail.festMatch?.myFestPower ?? undefined,
|
||||
} : undefined,
|
||||
xBattle: vsDetail.vsMode.mode === "X_MATCH" ? {
|
||||
xPower: vsDetail.xMatch?.lastXPower ?? undefined,
|
||||
} : undefined,
|
||||
challenge: vsDetail.vsMode.mode === "LEAGUE" ? {
|
||||
id: new TextDecoder().decode(base64.decode(vsDetail.leagueMatch?.leagueMatchEvent?.id!)).split("-")[1],
|
||||
power: vsDetail.leagueMatch?.myLeaguePower ?? undefined,
|
||||
} : undefined,
|
||||
teams: [],
|
||||
awards: vsDetail.awards.map((i) => i.name),
|
||||
};
|
||||
|
||||
const teams: VsTeam[] = [vsDetail.myTeam, ...vsDetail.otherTeams];
|
||||
|
||||
for (const team of teams) {
|
||||
const players = team.players.map(this.mapPlayer);
|
||||
const teamResult: Team = {
|
||||
players,
|
||||
color: team.color,
|
||||
isMyTeam: team.players.find((i) => i.isMyself) !== undefined,
|
||||
judgement: team.judgement as TeamJudgement,
|
||||
order: team.order as number,
|
||||
festStreakWinCount: team.festStreakWinCount as unknown as number ?? undefined,
|
||||
festTeamName: team.festTeamName ?? undefined,
|
||||
festUniformBonusRate: team.festUniformBonusRate as unknown as number ?? undefined,
|
||||
festUniformName: team.festUniformName as unknown as string ?? undefined,
|
||||
noroshi: team.result?.noroshi ?? undefined,
|
||||
paintRatio: team.result?.paintRatio ?? undefined,
|
||||
score: team.result?.score ?? undefined,
|
||||
tricolorRole: team.tricolorRole ?? undefined,
|
||||
}
|
||||
result.teams.push(teamResult);
|
||||
}
|
||||
|
||||
return {
|
||||
battle: result,
|
||||
data_type: "splashcat"
|
||||
}
|
||||
}
|
||||
mapColor(color: Color): string | undefined {
|
||||
const float2hex = (i: number) =>
|
||||
Math.round(i * 255).toString(16).padStart(2, "0");
|
||||
// rgba
|
||||
const nums = [color.r, color.g, color.b, color.a];
|
||||
return nums.map(float2hex).join("");
|
||||
}
|
||||
|
||||
mapGear(gear: PlayerGear): Gear {
|
||||
return {
|
||||
name: gear.name,
|
||||
primaryAbility: gear.primaryGearPower.name,
|
||||
secondaryAbilities: gear.additionalGearPowers.map((i) => i.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -366,7 +366,7 @@ export class StatInkExporter implements GameExporter {
|
|||
{ primaryGearPower, additionalGearPowers }: PlayerGear,
|
||||
): StatInkGear => {
|
||||
const primary = mapAbility(primaryGearPower);
|
||||
if (!primary && !this.isRandom(primaryGearPower.image)) {
|
||||
if (!primary) {
|
||||
throw new Error("Unknown ability: " + primaryGearPower.name);
|
||||
}
|
||||
return {
|
||||
|
|
@ -394,8 +394,6 @@ export class StatInkExporter implements GameExporter {
|
|||
inked: player.paint,
|
||||
gears: await this.mapGears(player),
|
||||
crown: player.crown ? "yes" : "no",
|
||||
crown_type: undefined,
|
||||
species: player.species === "INKLING" ? "inkling" : "octoling",
|
||||
disconnected: player.result ? "no" : "yes",
|
||||
};
|
||||
if (player.result) {
|
||||
|
|
@ -406,13 +404,6 @@ export class StatInkExporter implements GameExporter {
|
|||
result.signal = player.result.noroshiTry ?? undefined;
|
||||
result.special = player.result.special;
|
||||
}
|
||||
if (player.crown) {
|
||||
result.crown_type = "x";
|
||||
} else if (player.festDragonCert === "DRAGON") {
|
||||
result.crown_type = "100x";
|
||||
} else if (player.festDragonCert === "DOUBLE_DRAGON") {
|
||||
result.crown_type = "333x";
|
||||
}
|
||||
return result;
|
||||
};
|
||||
async mapBattle(
|
||||
|
|
@ -597,8 +588,6 @@ export class StatInkExporter implements GameExporter {
|
|||
}
|
||||
}
|
||||
|
||||
result.bankara_power_after = vsDetail.bankaraMatch?.bankaraPower?.power;
|
||||
|
||||
if (rankBeforeState && rankState) {
|
||||
result.rank_before_exp = rankBeforeState.rankPoint;
|
||||
result.rank_after_exp = rankState.rankPoint;
|
||||
|
|
@ -630,18 +619,16 @@ export class StatInkExporter implements GameExporter {
|
|||
}
|
||||
isRandom(image: Image | null): boolean {
|
||||
// question mark
|
||||
const RANDOM_FILENAME = [
|
||||
"473fffb2442075078d8bb7125744905abdeae651b6a5b7453ae295582e45f7d1",
|
||||
"dc937b59892604f5a86ac96936cd7ff09e25f18ae6b758e8014a24c7fa039e91",
|
||||
];
|
||||
const RANDOM_FILENAME =
|
||||
"473fffb2442075078d8bb7125744905abdeae651b6a5b7453ae295582e45f7d1";
|
||||
// file exporter will replace url to { pathname: string } | string
|
||||
const url = image?.url as ReturnType<typeof urlSimplify> | undefined | null;
|
||||
if (typeof url === "string") {
|
||||
return RANDOM_FILENAME.some((i) => url.includes(i));
|
||||
return url.includes(RANDOM_FILENAME);
|
||||
} else if (url === undefined || url === null) {
|
||||
return false;
|
||||
} else {
|
||||
return RANDOM_FILENAME.some((i) => url.pathname.includes(i));
|
||||
return url.pathname.includes(RANDOM_FILENAME);
|
||||
}
|
||||
}
|
||||
async mapCoopWeapon(
|
||||
|
|
@ -711,7 +698,6 @@ export class StatInkExporter implements GameExporter {
|
|||
rescued: rescuedCount,
|
||||
defeat_boss: defeatEnemyCount,
|
||||
disconnected: disconnected ? "yes" : "no",
|
||||
species: player.species === "INKLING" ? "inkling" : "octoling",
|
||||
};
|
||||
}
|
||||
mapKing(id?: string) {
|
||||
|
|
|
|||
|
|
@ -213,20 +213,20 @@ export async function getGToken(
|
|||
|
||||
const idToken2: string = respJson?.result?.webApiServerCredential
|
||||
?.accessToken;
|
||||
const coralUserId: string = respJson?.result?.user?.id?.toString();
|
||||
const coralUserId: number = respJson?.result?.user?.id;
|
||||
|
||||
if (!idToken2 || !coralUserId) {
|
||||
throw new APIError({
|
||||
response: resp,
|
||||
json: respJson,
|
||||
message:
|
||||
`No idToken2 or coralUserId found. Please try again later. (${idToken2.length}, ${coralUserId.length})`,
|
||||
`No idToken2 or coralUserId found. Please try again later. ('${idToken2}', '${coralUserId}')`,
|
||||
});
|
||||
}
|
||||
|
||||
return [idToken2, coralUserId] as const;
|
||||
};
|
||||
const getGToken = async (idToken: string, coralUserId: string) => {
|
||||
const getGToken = async (idToken: string, coralUserId: number) => {
|
||||
const { f, request_id: requestId, timestamp } = await callImink({
|
||||
step: 2,
|
||||
idToken,
|
||||
|
|
@ -414,7 +414,7 @@ async function callImink(
|
|||
step: number;
|
||||
idToken: string;
|
||||
userId: string;
|
||||
coralUserId?: string;
|
||||
coralUserId?: number;
|
||||
env: Env;
|
||||
},
|
||||
): Promise<IminkResponse> {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
} from "./types.ts";
|
||||
import { DEFAULT_ENV, Env } from "./env.ts";
|
||||
import { getBulletToken, getGToken } from "./iksm.ts";
|
||||
import { battleTime, parseHistoryDetailId } from "./utils.ts";
|
||||
import { parseHistoryDetailId } from "./utils.ts";
|
||||
|
||||
export class Splatnet3 {
|
||||
protected profile: Profile;
|
||||
|
|
@ -137,12 +137,6 @@ export class Splatnet3 {
|
|||
[BattleListType.Bankara]: () =>
|
||||
this.request(Queries.BankaraBattleHistoriesQuery)
|
||||
.then((r) => getIdsFromGroups(r.bankaraBattleHistories)),
|
||||
[BattleListType.XBattle]: () =>
|
||||
this.request(Queries.XBattleHistoriesQuery)
|
||||
.then((r) => getIdsFromGroups(r.xBattleHistories)),
|
||||
[BattleListType.Event]: () =>
|
||||
this.request(Queries.EventBattleHistoriesQuery)
|
||||
.then((r) => getIdsFromGroups(r.eventBattleHistories)),
|
||||
[BattleListType.Private]: () =>
|
||||
this.request(Queries.PrivateBattleHistoriesQuery)
|
||||
.then((r) => getIdsFromGroups(r.privateBattleHistories)),
|
||||
|
|
@ -174,29 +168,6 @@ export class Splatnet3 {
|
|||
return await this.BATTLE_LIST_TYPE_MAP[battleListType]();
|
||||
}
|
||||
|
||||
// Get all id from all battle list, sort by time, [0] is the latest
|
||||
async getAllBattleList() {
|
||||
const ALL_TYPE: BattleListType[] = [
|
||||
BattleListType.Regular,
|
||||
BattleListType.Bankara,
|
||||
BattleListType.XBattle,
|
||||
BattleListType.Event,
|
||||
BattleListType.Private,
|
||||
];
|
||||
const ids: string[] = [];
|
||||
for (const type of ALL_TYPE) {
|
||||
ids.push(...await this.getBattleList(type));
|
||||
}
|
||||
|
||||
const timeMap = new Map<string, Date>(
|
||||
ids.map((id) => [id, battleTime(id)] as const),
|
||||
);
|
||||
|
||||
return ids.sort((a, b) =>
|
||||
timeMap.get(b)!.getTime() - timeMap.get(a)!.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
getBattleDetail(
|
||||
id: string,
|
||||
) {
|
||||
|
|
|
|||
31
src/types.ts
31
src/types.ts
|
|
@ -9,7 +9,6 @@ export type VarsMap = {
|
|||
[Queries.RegularBattleHistoriesQuery]: [];
|
||||
[Queries.BankaraBattleHistoriesQuery]: [];
|
||||
[Queries.XBattleHistoriesQuery]: [];
|
||||
[Queries.EventBattleHistoriesQuery]: [];
|
||||
[Queries.PrivateBattleHistoriesQuery]: [];
|
||||
[Queries.VsHistoryDetailQuery]: [{
|
||||
vsResultId: string;
|
||||
|
|
@ -131,7 +130,6 @@ export type PlayerWeapon = {
|
|||
};
|
||||
};
|
||||
export type VsPlayer = {
|
||||
[x: string]: Nameplate;
|
||||
id: string;
|
||||
nameId: string | null;
|
||||
name: string;
|
||||
|
|
@ -148,7 +146,6 @@ export type VsPlayer = {
|
|||
} | null;
|
||||
paint: number;
|
||||
crown: boolean;
|
||||
festDragonCert: "NONE" | "DRAGON" | "DOUBLE_DRAGON";
|
||||
|
||||
headGear: PlayerGear;
|
||||
clothingGear: PlayerGear;
|
||||
|
|
@ -161,11 +158,6 @@ export type Color = {
|
|||
r: number;
|
||||
};
|
||||
export type VsTeam = {
|
||||
festUniformName: undefined;
|
||||
festUniformBonusRate: unknown;
|
||||
festStreakWinCount: undefined;
|
||||
order: unknown;
|
||||
judgement: string;
|
||||
players: VsPlayer[];
|
||||
color: Color;
|
||||
tricolorRole: null | "DEFENSE" | "ATTACK1" | "ATTACK2";
|
||||
|
|
@ -173,7 +165,6 @@ order: unknown;
|
|||
result: null | {
|
||||
paintRatio: null | number;
|
||||
score: null | number;
|
||||
noroshi: null | number;
|
||||
};
|
||||
};
|
||||
export type VsRule =
|
||||
|
|
@ -246,9 +237,6 @@ export type VsHistoryDetail = {
|
|||
bankaraMatch: {
|
||||
earnedUdemaePoint: null | number;
|
||||
mode: "OPEN" | "CHALLENGE";
|
||||
bankaraPower?: null | {
|
||||
power?: null | number;
|
||||
};
|
||||
} | null;
|
||||
festMatch: {
|
||||
dragonMatchType: "NORMAL" | "DECUPLE" | "DRAGON" | "DOUBLE_DRAGON";
|
||||
|
|
@ -280,8 +268,6 @@ export type CoopHistoryPlayerResult = {
|
|||
name: string;
|
||||
id: string;
|
||||
};
|
||||
isMyself: boolean;
|
||||
species: "INKLING" | "OCTOLING";
|
||||
};
|
||||
weapons: { name: string; image: Image | null }[];
|
||||
specialWeapon: null | {
|
||||
|
|
@ -434,11 +420,6 @@ export type RespMap = {
|
|||
};
|
||||
[Queries.BankaraBattleHistoriesQuery]: BankaraBattleHistories;
|
||||
[Queries.XBattleHistoriesQuery]: XBattleHistories;
|
||||
[Queries.EventBattleHistoriesQuery]: {
|
||||
eventBattleHistories: {
|
||||
historyGroups: HistoryGroups<BattleListNode>;
|
||||
};
|
||||
};
|
||||
[Queries.PrivateBattleHistoriesQuery]: {
|
||||
privateBattleHistories: {
|
||||
historyGroups: HistoryGroups<BattleListNode>;
|
||||
|
|
@ -626,14 +607,10 @@ export enum BattleListType {
|
|||
Latest,
|
||||
Regular,
|
||||
Bankara,
|
||||
Event,
|
||||
XBattle,
|
||||
Private,
|
||||
Coop,
|
||||
}
|
||||
|
||||
export type ListMethod = "latest" | "all" | "auto";
|
||||
|
||||
export type StatInkUuidList = {
|
||||
status: number;
|
||||
code: number;
|
||||
|
|
@ -653,7 +630,7 @@ export type StatInkWeapon = {
|
|||
}[];
|
||||
|
||||
export type StatInkGear = {
|
||||
primary_ability: string | null;
|
||||
primary_ability: string;
|
||||
secondary_abilities: (string | null)[];
|
||||
};
|
||||
|
||||
|
|
@ -679,9 +656,7 @@ export type StatInkPlayer = {
|
|||
special?: number;
|
||||
gears?: StatInkGears;
|
||||
crown?: "yes" | "no";
|
||||
crown_type?: "x" | "100x" | "333x";
|
||||
disconnected: "yes" | "no";
|
||||
species: "inkling" | "octoling";
|
||||
};
|
||||
|
||||
export type StatInkStage = {
|
||||
|
|
@ -731,7 +706,6 @@ export type StatInkCoopPlayer = {
|
|||
rescued: number;
|
||||
defeat_boss: number;
|
||||
disconnected: "yes" | "no";
|
||||
species: "inkling" | "octoling";
|
||||
};
|
||||
|
||||
export type StatInkCoopBoss = {
|
||||
|
|
@ -785,7 +759,6 @@ export type StatInkCoopPostBody = {
|
|||
};
|
||||
|
||||
export type StatInkPostBody = {
|
||||
link_url?: string;
|
||||
test?: "yes" | "no";
|
||||
uuid: string;
|
||||
lobby:
|
||||
|
|
@ -842,8 +815,6 @@ export type StatInkPostBody = {
|
|||
challenge_lose?: number;
|
||||
x_power_before?: number | null;
|
||||
x_power_after?: number | null;
|
||||
bankara_power_before?: number | null;
|
||||
bankara_power_after?: number | null;
|
||||
fest_power?: number; // Splatfest Power (Pro)
|
||||
fest_dragon?:
|
||||
| "10x"
|
||||
|
|
|
|||
11
src/utils.ts
11
src/utils.ts
|
|
@ -188,14 +188,3 @@ export function urlSimplify(url: string): { pathname: string } | string {
|
|||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export const battleTime = (id: string) => {
|
||||
const { timestamp } = parseHistoryDetailId(id);
|
||||
|
||||
const dateStr = timestamp.replace(
|
||||
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/,
|
||||
"$1-$2-$3T$4:$5:$6Z",
|
||||
);
|
||||
|
||||
return new Date(dateStr);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue