push: ALL.Net and a little of CHUNITHM
This commit is contained in:
parent
8186d6b488
commit
3cffcd1410
|
@ -143,6 +143,8 @@ dist
|
||||||
|
|
||||||
# End of https://www.toptal.com/developers/gitignore/api/node
|
# End of https://www.toptal.com/developers/gitignore/api/node
|
||||||
|
|
||||||
|
data
|
||||||
|
logs
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.sqlite3-shm
|
*.sqlite3-shm
|
||||||
*.sqlite3-wal
|
*.sqlite3-wal
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
// Used as ALL.Net arcade name for unregistered arcades
|
||||||
|
NAME: "KozukataToa",
|
||||||
|
DATABASE_PATH: "data/db.sqlite3",
|
||||||
|
LISTEN_ADDRESS: "0.0.0.0",
|
||||||
|
LOGGER_CONFIG: {
|
||||||
|
LOG_LEVEL: "debug",
|
||||||
|
CONSOLE: true,
|
||||||
|
// Enter a valid path to save log files.
|
||||||
|
FOLDER: "",
|
||||||
|
},
|
||||||
|
ALLNET_CONFIG: {
|
||||||
|
ENABLED: true,
|
||||||
|
PORT: 80,
|
||||||
|
ALLOW_UNREGISTERED_SERIALS: true,
|
||||||
|
// Only used for network delivery.
|
||||||
|
HOSTNAME: "localhost",
|
||||||
|
// Enter a valid path here to enable remote updates.
|
||||||
|
UPDATE_CFG_FOLDER: "",
|
||||||
|
},
|
||||||
|
AIMEDB_CONFIG: {
|
||||||
|
ENABLED: true,
|
||||||
|
PORT: 22345,
|
||||||
|
// Really. That's the key.
|
||||||
|
KEY: "Copyright(C)SEGA",
|
||||||
|
// These keys are for generating old-style access codes from FeliCa IDms.
|
||||||
|
// If you want to roll your own keys, just shuffle the 16 hexadecimal characters.
|
||||||
|
AIME_MOBILE_CARD_KEY: "5CD3E81B9024F67A",
|
||||||
|
RESERVED_CARD_PREFIX: "01054",
|
||||||
|
RESERVED_CARD_KEY: "82DAF451B3E076C9",
|
||||||
|
},
|
||||||
|
TITLES_CONFIG: {
|
||||||
|
ENABLED: true,
|
||||||
|
PORT: 8080,
|
||||||
|
// You may need to change this if title server connection is BAD. Some games
|
||||||
|
// explicitly disallows localhost and similar addresses, but other games don't.
|
||||||
|
HOSTNAME: "localhost",
|
||||||
|
},
|
||||||
|
CHUNITHM_CONFIG: {
|
||||||
|
ENABLE: true,
|
||||||
|
MODS: {
|
||||||
|
// Enter a name to display as team in CHUNTHM.
|
||||||
|
TEAM_NAME: "",
|
||||||
|
// Enable login bonuses
|
||||||
|
USE_LOGIN_BONUS: true,
|
||||||
|
// You have everything.
|
||||||
|
FORCE_UNLOCK_ALL: false,
|
||||||
|
},
|
||||||
|
// The ROM and data version to report to the game. This must match your game,
|
||||||
|
// or else you will not be able to play some game modes.
|
||||||
|
// - ROM version: Match with "GAME SYSTEM INFORMATION" in test menu.
|
||||||
|
// - Data version: Match with data.conf file in latest option (last in alphabetical order)
|
||||||
|
VERSIONS: {
|
||||||
|
"200": {
|
||||||
|
rom: "2.00.00",
|
||||||
|
data: "2.00.00",
|
||||||
|
},
|
||||||
|
"205": {
|
||||||
|
rom: "2.05.00",
|
||||||
|
data: "2.05.00",
|
||||||
|
},
|
||||||
|
"210": {
|
||||||
|
rom: "2.10.00",
|
||||||
|
data: "2.10.00",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Since CRYSTAL+, network communications are encrypted and the endpoint is hashed.
|
||||||
|
// Enter the [key, iv, salt] used for that here.
|
||||||
|
// Optionally, set ENCRYPTED_ONLY to true to disallow unencrypted network communications.
|
||||||
|
CRYPTO: {
|
||||||
|
ENCRYPTED_ONLY: false,
|
||||||
|
KEYS: {
|
||||||
|
"210": [
|
||||||
|
"75695c3d265f434c3953454c5830522b4b3c4d7b42482a312e5627216b2b4060",
|
||||||
|
"31277c37707044377141595058345a6b",
|
||||||
|
"04780206ca5f36f4",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,11 +26,10 @@
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-errors": "^3.1.1",
|
"express-async-errors": "^3.1.1",
|
||||||
"fletcher": "^0.0.3",
|
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"micro-packed": "^0.3.2",
|
"on-headers": "^1.0.2",
|
||||||
"raw-body": "^2.5.2",
|
"raw-body": "^2.5.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"safe-json-stringify": "^1.2.0",
|
"safe-json-stringify": "^1.2.0",
|
||||||
|
@ -50,6 +49,7 @@
|
||||||
"@types/iconv": "^3.0.4",
|
"@types/iconv": "^3.0.4",
|
||||||
"@types/luxon": "^3.3.4",
|
"@types/luxon": "^3.3.4",
|
||||||
"@types/node": "16",
|
"@types/node": "16",
|
||||||
|
"@types/on-headers": "^1.0.3",
|
||||||
"@types/safe-json-stringify": "^1.1.5",
|
"@types/safe-json-stringify": "^1.1.5",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.47.1",
|
||||||
|
|
|
@ -7,6 +7,7 @@ specifiers:
|
||||||
'@types/iconv': ^3.0.4
|
'@types/iconv': ^3.0.4
|
||||||
'@types/luxon': ^3.3.4
|
'@types/luxon': ^3.3.4
|
||||||
'@types/node': '16'
|
'@types/node': '16'
|
||||||
|
'@types/on-headers': ^1.0.3
|
||||||
'@types/safe-json-stringify': ^1.1.5
|
'@types/safe-json-stringify': ^1.1.5
|
||||||
'@typescript-eslint/eslint-plugin': 5.47.1
|
'@typescript-eslint/eslint-plugin': 5.47.1
|
||||||
'@typescript-eslint/parser': 5.47.1
|
'@typescript-eslint/parser': 5.47.1
|
||||||
|
@ -21,10 +22,12 @@ specifiers:
|
||||||
json5: ^2.2.3
|
json5: ^2.2.3
|
||||||
luxon: ^3.4.4
|
luxon: ^3.4.4
|
||||||
micro-packed: ^0.3.2
|
micro-packed: ^0.3.2
|
||||||
|
on-headers: ^1.0.2
|
||||||
raw-body: ^2.5.2
|
raw-body: ^2.5.2
|
||||||
reflect-metadata: ^0.1.13
|
reflect-metadata: ^0.1.13
|
||||||
safe-json-stringify: ^1.2.0
|
safe-json-stringify: ^1.2.0
|
||||||
sqlite3: ^5.1.6
|
sqlite3: ^5.1.6
|
||||||
|
stream-combiner: ^0.2.2
|
||||||
tap: ^18.6.1
|
tap: ^18.6.1
|
||||||
tsconfig-paths: ^4.2.0
|
tsconfig-paths: ^4.2.0
|
||||||
typed-struct: ^2.3.0
|
typed-struct: ^2.3.0
|
||||||
|
@ -45,10 +48,12 @@ dependencies:
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
luxon: 3.4.4
|
luxon: 3.4.4
|
||||||
micro-packed: 0.3.2
|
micro-packed: 0.3.2
|
||||||
|
on-headers: 1.0.2
|
||||||
raw-body: 2.5.2
|
raw-body: 2.5.2
|
||||||
reflect-metadata: 0.1.13
|
reflect-metadata: 0.1.13
|
||||||
safe-json-stringify: 1.2.0
|
safe-json-stringify: 1.2.0
|
||||||
sqlite3: 5.1.6
|
sqlite3: 5.1.6
|
||||||
|
stream-combiner: 0.2.2
|
||||||
tsconfig-paths: 4.2.0
|
tsconfig-paths: 4.2.0
|
||||||
typed-struct: 2.3.0_iconv-lite@0.6.3
|
typed-struct: 2.3.0_iconv-lite@0.6.3
|
||||||
typeorm: 0.3.17_sqlite3@5.1.6
|
typeorm: 0.3.17_sqlite3@5.1.6
|
||||||
|
@ -64,6 +69,7 @@ devDependencies:
|
||||||
'@types/iconv': 3.0.4
|
'@types/iconv': 3.0.4
|
||||||
'@types/luxon': 3.3.4
|
'@types/luxon': 3.3.4
|
||||||
'@types/node': 16.18.62
|
'@types/node': 16.18.62
|
||||||
|
'@types/on-headers': 1.0.3
|
||||||
'@types/safe-json-stringify': 1.1.5
|
'@types/safe-json-stringify': 1.1.5
|
||||||
'@typescript-eslint/eslint-plugin': 5.47.1_o6yrxajvsx2b7l3udqdd2yq4ii
|
'@typescript-eslint/eslint-plugin': 5.47.1_o6yrxajvsx2b7l3udqdd2yq4ii
|
||||||
'@typescript-eslint/parser': 5.47.1_4njjt2tu6ubn7rwbgcnueihm54
|
'@typescript-eslint/parser': 5.47.1_4njjt2tu6ubn7rwbgcnueihm54
|
||||||
|
@ -942,6 +948,12 @@ packages:
|
||||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/on-headers/1.0.3:
|
||||||
|
resolution: {integrity: sha512-jvGNvFo8uOL6fiBGvD4Ul4lT8mZoJ57l3h0ZN/a1oHziTTXUV3slaRcYm2K1wvvLX1fhIg9AvKykxKFt3mM+Xg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 16.18.62
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/qs/6.9.10:
|
/@types/qs/6.9.10:
|
||||||
resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==}
|
resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1825,6 +1837,10 @@ packages:
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/duplexer/0.1.2:
|
||||||
|
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/eastasianwidth/0.2.0:
|
/eastasianwidth/0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4560,6 +4576,13 @@ packages:
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/stream-combiner/0.2.2:
|
||||||
|
resolution: {integrity: sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==}
|
||||||
|
dependencies:
|
||||||
|
duplexer: 0.1.2
|
||||||
|
through: 2.3.8
|
||||||
|
dev: false
|
||||||
|
|
||||||
/string-length/6.0.0:
|
/string-length/6.0.0:
|
||||||
resolution: {integrity: sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==}
|
resolution: {integrity: sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
@ -4792,6 +4815,10 @@ packages:
|
||||||
any-promise: 1.3.0
|
any-promise: 1.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/through/2.3.8:
|
||||||
|
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-regex-range/5.0.1:
|
/to-regex-range/5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
|
@ -1,26 +1,14 @@
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { AimeCard, AimeUser, FelicaCardLookup, FelicaMobileLookup } from "./entity/aimedb";
|
|
||||||
import { Arcade, Machine } from "./entity/allnet";
|
|
||||||
import { EventLog } from "./entity/base";
|
|
||||||
import { ChunithmStaticEvent } from "./entity/chunithm/static";
|
|
||||||
import { Config } from "lib/setup/config";
|
import { Config } from "lib/setup/config";
|
||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: "sqlite",
|
type: "sqlite",
|
||||||
database: Config.DATABASE_PATH,
|
database: Config.DATABASE_PATH,
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
entities: [
|
entities: [join(__dirname, "entity", "**", "*.{ts,js}")],
|
||||||
Arcade,
|
|
||||||
AimeCard,
|
|
||||||
AimeUser,
|
|
||||||
ChunithmStaticEvent,
|
|
||||||
EventLog,
|
|
||||||
FelicaCardLookup,
|
|
||||||
FelicaMobileLookup,
|
|
||||||
Machine,
|
|
||||||
],
|
|
||||||
migrations: [],
|
migrations: [],
|
||||||
subscribers: [],
|
subscribers: [],
|
||||||
enableWAL: true,
|
enableWAL: true,
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { AimeCard } from "../aimedb";
|
||||||
|
import { ConstructableBaseEntity } from "external/db/utils/constructable-base-entity";
|
||||||
|
import { HasIdCardVersion } from "external/db/utils/has-id-card-version";
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
@Entity("chunithm_user_character")
|
||||||
|
@Unique(["card", "characterId"])
|
||||||
|
export class ChunithmUserCharacter extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@ManyToOne(() => AimeCard, { eager: true })
|
||||||
|
card!: AimeCard;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
characterId!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
skillId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
level!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
param1!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
param2!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: true })
|
||||||
|
isValid!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: true })
|
||||||
|
isNewMark!: boolean;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
playCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
friendshipExp!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
assignIllust!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
exMaxLv!: integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("chunithm_user_item")
|
||||||
|
@Unique(["card", "itemId"])
|
||||||
|
export class ChunithmUserItem extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@ManyToOne(() => AimeCard, { eager: true })
|
||||||
|
card!: AimeCard;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
itemId!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
itemKind!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
stock!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: true })
|
||||||
|
isValid!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("chunithm_user_favorite_item")
|
||||||
|
@Unique(["card", "version", "favId", "favKind"])
|
||||||
|
export class ChunithmUserFavoriteItem extends HasIdCardVersion {
|
||||||
|
@Column()
|
||||||
|
favId!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
favKind!: integer;
|
||||||
|
}
|
|
@ -0,0 +1,753 @@
|
||||||
|
import { AimeCard } from "../aimedb";
|
||||||
|
import { ConstructableBaseEntity } from "external/db/utils/constructable-base-entity";
|
||||||
|
import { HasIdCardVersion } from "external/db/utils/has-id-card-version";
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
@Entity("chunithm_user_data")
|
||||||
|
@Unique(["card", "version"])
|
||||||
|
export class ChunithmUserData extends HasIdCardVersion {
|
||||||
|
@Column({ type: "text" })
|
||||||
|
userName!: string;
|
||||||
|
|
||||||
|
@Column({ type: "text" })
|
||||||
|
userNameEx!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since NEW
|
||||||
|
*/
|
||||||
|
@Column({ default: 0 })
|
||||||
|
teamId!: integer;
|
||||||
|
|
||||||
|
// #region Rating
|
||||||
|
@Column({ default: 0 })
|
||||||
|
playerRating!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
highestRating!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region OVER POWER
|
||||||
|
@Column({ default: 0 })
|
||||||
|
overPowerPoint!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
overPowerRate!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
overPowerLowerRank!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region User level
|
||||||
|
@Column({ default: 0 })
|
||||||
|
reincarnationNum!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
level!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
exp!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Course mode
|
||||||
|
@Column({ default: 0 })
|
||||||
|
classEmblemBase!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
classEmblemMedal!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Play count
|
||||||
|
@Column({ default: 0 })
|
||||||
|
playCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
multiPlayCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
multiWinCount!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Currency
|
||||||
|
@Column({ default: 0 })
|
||||||
|
point!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalPoint!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
avatarPoint!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Collections
|
||||||
|
@Column({ default: 1 })
|
||||||
|
nameplateId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
frameId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
characterId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
trophyId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
voiceId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
mapIconId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
charaIllustId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 100000 })
|
||||||
|
skillId!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region User avatar
|
||||||
|
@Column({ default: 1100001 })
|
||||||
|
avatarWear!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1200001 })
|
||||||
|
avatarHead!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1300001 })
|
||||||
|
avatarFace!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1400001 })
|
||||||
|
avatarSkin!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1500001 })
|
||||||
|
avatarItem!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1600001 })
|
||||||
|
avatarFront!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1700001 })
|
||||||
|
avatarBack!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Tutorial
|
||||||
|
@Column({ default: 0 })
|
||||||
|
playedTutorialBit!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
firstTutorialCancelNum!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
masterTutorialCancelNum!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Cummulative statistics
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalMapNum!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalHiScore!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalBasicHighScore!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalAdvancedHighScore!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalExpertHighScore!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalMasterHighScore!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
totalRepertoireCount!: integer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since NEW
|
||||||
|
*/
|
||||||
|
@Column({ type: "integer", nullable: true, default: null })
|
||||||
|
totalUltimaHighScore!: integer | null;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Map
|
||||||
|
@Column({ default: 0 })
|
||||||
|
stockedGridCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
exMapLoopCount!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Online battle
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattlePlayCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattleWinCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattleLoseCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattleConsecutiveWinCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
battleRankId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
battleRankPoint!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
eliteRankPoint!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattle1stCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattle2ndCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattle3rdCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattle4thCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattleCorrection!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattleErrCnt!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattleHostErrCnt!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
battleRewardStatus!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
battleRewardIndex!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
battleRewardCount!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: false })
|
||||||
|
isNetBattleHost!: boolean;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
netBattleEndState!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Play location history
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
firstGameId!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
firstRomVersion!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
firstDataVersion!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "datetime", nullable: true })
|
||||||
|
firstPlayDate!: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
lastGameId!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
lastRomVersion!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
lastDataVersion!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "datetime", nullable: true })
|
||||||
|
lastPlayDate!: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: "integer", nullable: true })
|
||||||
|
lastPlaceId!: integer | null;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 60, nullable: true })
|
||||||
|
lastPlaceName!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "integer", nullable: true })
|
||||||
|
lastRegionId!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 48, nullable: true })
|
||||||
|
lastRegionName!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "integer", nullable: true })
|
||||||
|
lastAllNetId!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 11, nullable: true })
|
||||||
|
lastClientId!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 3, nullable: true })
|
||||||
|
lastCountryCode!: string | null;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region what the hell is a res
|
||||||
|
@Column({ default: 0 })
|
||||||
|
acceptResCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
requestResCount!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
successResCount!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Other fields
|
||||||
|
/**
|
||||||
|
* @deprecated since NEW
|
||||||
|
*/
|
||||||
|
@Column({ type: "boolean", default: false })
|
||||||
|
isMaimai!: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated since NEW
|
||||||
|
*/
|
||||||
|
@Column({ type: "boolean", default: false })
|
||||||
|
isWebJoin!: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "datetime", nullable: true, default: null })
|
||||||
|
eventWatchedDate!: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: "datetime", nullable: true, default: null })
|
||||||
|
webLimitDate!: Date | null;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
friendCount!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
compatibleCmVersion!: string | null;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
medal!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Extension columns
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext1!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext2!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext3!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext4!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext5!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext6!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext7!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext8!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext9!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext10!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
extStr1!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
extStr2!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
extLong1!: string;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
extLong2!: string;
|
||||||
|
// #endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("chunithm_user_data_ex")
|
||||||
|
@Unique(["card", "version"])
|
||||||
|
export class ChunithmUserDataEx extends HasIdCardVersion {
|
||||||
|
@Column({ default: 0 })
|
||||||
|
medal!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
voiceId!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
mapIconId!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
compatibleCmVersion!: string | null;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext1!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext2!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext3!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext4!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext5!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext6!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext7!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext8!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext9!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext10!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext11!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext12!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext13!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext14!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext15!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext16!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext17!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext18!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext19!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext20!: integer;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
extStr1!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
extStr2!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
extStr3!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
extStr4!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "text", nullable: true })
|
||||||
|
extStr5!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
extLong1!: string;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
extLong2!: string;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
extLong3!: string;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
extLong4!: string;
|
||||||
|
|
||||||
|
@Column({ type: "bigint", nullable: true })
|
||||||
|
extLong5!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("chunithm_user_option")
|
||||||
|
@Unique(["card", "version"])
|
||||||
|
export class ChunithmUserOption extends HasIdCardVersion {
|
||||||
|
/**
|
||||||
|
* Options preset selected.
|
||||||
|
*/
|
||||||
|
@Column({ default: 0 })
|
||||||
|
optionSet!: integer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Headphone volume.
|
||||||
|
*/
|
||||||
|
@Column({ default: 0 })
|
||||||
|
headphone!: integer;
|
||||||
|
|
||||||
|
// what??
|
||||||
|
@Column({ default: 0 })
|
||||||
|
hardJudge!: integer;
|
||||||
|
|
||||||
|
// #region Game settings
|
||||||
|
/**
|
||||||
|
* @note This may look like speed 5, but not really. CHUNITHM stores note speed in an enum.
|
||||||
|
* The actual note speed is `speed / 4 + 1`. Speed 2 matches the beginner speed in-game.
|
||||||
|
*/
|
||||||
|
@Column({ default: 4 })
|
||||||
|
speed!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 4 })
|
||||||
|
speed_120!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
mirrorFumen!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
trackSkip!: integer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note Displayed in game as JUDGEMENT TIMING A. Default +0.0
|
||||||
|
*/
|
||||||
|
@Column({ default: 20 })
|
||||||
|
playTimingOffset!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 20 })
|
||||||
|
playTimingOffset_120!: integer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @note Displayed in game as JUDGEMENT TIMING B. Default +0.0
|
||||||
|
*/
|
||||||
|
@Column({ default: 20 })
|
||||||
|
judgeTimingOffset!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 20 })
|
||||||
|
judgeTimingOffset_120!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
matching!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
playerLevel!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
rating!: integer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display OVER POWER details on the song selection menu.
|
||||||
|
*/
|
||||||
|
@Column({ default: 0 })
|
||||||
|
categoryDetail!: integer;
|
||||||
|
|
||||||
|
// wtf is this???
|
||||||
|
@Column({ default: 0 })
|
||||||
|
privacy!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Sound settings
|
||||||
|
@Column({ default: 5 })
|
||||||
|
guideSound!: integer;
|
||||||
|
|
||||||
|
// Sound effect played on TAP
|
||||||
|
@Column({ default: 0 })
|
||||||
|
successTapTimbre!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 5 })
|
||||||
|
successTap!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 5 })
|
||||||
|
successExTap!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 5 })
|
||||||
|
successSlideHold!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 5 })
|
||||||
|
successAir!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 5 })
|
||||||
|
successFlick!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
soundEffect!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
judgeAppendSe!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
resultVoiceShort!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Judgement display settings
|
||||||
|
@Column({ default: 1 })
|
||||||
|
judgePos!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 2 })
|
||||||
|
judgeAttack!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 3 })
|
||||||
|
judgeJustice!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 3 })
|
||||||
|
judgeCritical!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Sort settings
|
||||||
|
@Column({ default: 1 })
|
||||||
|
selectMusicFilterLv!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
sortMusicFilterLv!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
sortMusicGenre!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Background display settings
|
||||||
|
@Column({ default: 2 })
|
||||||
|
guideLine!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
fieldColor!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
fieldWallPosition!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
fieldWallPosition_120!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
bgInfo!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
notesThickness!: integer;
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
// #region Extension columns
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext1!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext2!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext3!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext4!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext5!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext6!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext7!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext8!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext9!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext10!: integer;
|
||||||
|
// #endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("chunithm_user_option_ex")
|
||||||
|
@Unique(["card", "version"])
|
||||||
|
export class ChunithmUserOptionEx extends HasIdCardVersion {
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext1!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext2!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext3!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext4!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext5!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext6!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext7!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext8!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext9!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext10!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext11!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext12!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext13!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext14!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext15!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext16!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext17!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext18!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext19!: integer;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
ext20!: integer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity("chunithm_user_activity")
|
||||||
|
export class ChunithmUserActivity extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@ManyToOne(() => AimeCard, { eager: true })
|
||||||
|
card!: AimeCard;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
activityId!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
kind!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
sortNumber!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
param1!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
param2!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
param3!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
param4!: integer;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { AimeCard } from "../aimedb";
|
||||||
|
import { ConstructableBaseEntity } from "external/db/utils/constructable-base-entity";
|
||||||
|
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, Unique } from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
@Unique(["card", "version", "order"])
|
||||||
|
export class ChunithmUserRecentRating extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@ManyToOne(() => AimeCard, { eager: true })
|
||||||
|
card!: AimeCard;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
version!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
order!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
musicId!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
difficultId!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
romVersionCode!: integer;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
score!: integer;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ConstructableBaseEntity } from "./constructable-base-entity";
|
||||||
|
import { AimeCard } from "../entity/aimedb";
|
||||||
|
import { PrimaryGeneratedColumn, ManyToOne, Column } from "typeorm";
|
||||||
|
import { integer } from "types/misc";
|
||||||
|
|
||||||
|
export class HasIdCardVersion extends ConstructableBaseEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: integer;
|
||||||
|
|
||||||
|
@ManyToOne(() => AimeCard, { eager: true })
|
||||||
|
card!: AimeCard;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
version!: integer;
|
||||||
|
}
|
|
@ -36,12 +36,21 @@ const configSchema = z.object({
|
||||||
CONSOLE: z.boolean().default(true),
|
CONSOLE: z.boolean().default(true),
|
||||||
FOLDER: z.string().default("logs"),
|
FOLDER: z.string().default("logs"),
|
||||||
}),
|
}),
|
||||||
ALLNET_CONFIG: z.object({
|
ALLNET_CONFIG: z
|
||||||
ENABLED: z.boolean().default(true),
|
.object({
|
||||||
PORT: zod16bitNumber.default(80),
|
ENABLED: z.boolean().default(true),
|
||||||
ALLOW_UNREGISTERED_SERIALS: z.boolean().default(true),
|
PORT: zod16bitNumber.default(80),
|
||||||
UPDATE_CFG_FOLDER: z.string().optional(),
|
ALLOW_UNREGISTERED_SERIALS: z.boolean().default(true),
|
||||||
}),
|
UPDATE_CFG_FOLDER: z.string().optional(),
|
||||||
|
HOSTNAME: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine((c) => {
|
||||||
|
if (c.UPDATE_CFG_FOLDER) {
|
||||||
|
return !!c.HOSTNAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, "Must specify ALL.Net hostname if allowing updates."),
|
||||||
AIMEDB_CONFIG: z.object({
|
AIMEDB_CONFIG: z.object({
|
||||||
ENABLED: z.boolean().default(true),
|
ENABLED: z.boolean().default(true),
|
||||||
PORT: zod16bitNumber.default(22345),
|
PORT: zod16bitNumber.default(22345),
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { CommandId, PortalRegistration, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
import {
|
import {
|
||||||
AimeAccountQueryStruct,
|
AimeAccountQueryStruct,
|
||||||
AimeAccountResponseStruct,
|
AimeAccountResponseStruct,
|
||||||
|
@ -5,7 +6,6 @@ import {
|
||||||
AimeAccountExtendedQueryStruct,
|
AimeAccountExtendedQueryStruct,
|
||||||
} from "../types/aime-account";
|
} from "../types/aime-account";
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CommandId, PortalRegistration, ResultCodes } from "../../../lib/constants/aimedb";
|
|
||||||
import { AimeCard } from "external/db/entity/aimedb";
|
import { AimeCard } from "external/db/entity/aimedb";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
@ -35,6 +35,7 @@ export const GetAimeAccountHandler: AimeDBHandlerFn<"AimeAccountResponse"> = asy
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Verify access code validity
|
||||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||||
|
|
||||||
const card = await AimeCard.findOne({
|
const card = await AimeCard.findOne({
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { CommandId, LogStatus, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
import {
|
import {
|
||||||
AimeLogStruct,
|
AimeLogStruct,
|
||||||
AimeLogExtendedResponseStruct,
|
AimeLogExtendedResponseStruct,
|
||||||
|
@ -5,7 +6,6 @@ import {
|
||||||
StatusLogStruct,
|
StatusLogStruct,
|
||||||
} from "../types/aime-log";
|
} from "../types/aime-log";
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CommandId, LogStatus, ResultCodes } from "../../../lib/constants/aimedb";
|
|
||||||
import { AppDataSource } from "external/db/data-source";
|
import { AppDataSource } from "external/db/data-source";
|
||||||
import { EventLog } from "external/db/entity/base";
|
import { EventLog } from "external/db/entity/base";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// TODO: Actually support campaigns
|
// TODO: Actually support campaigns
|
||||||
|
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
import {
|
import {
|
||||||
CampaignClearInfoResponseStruct,
|
CampaignClearInfoResponseStruct,
|
||||||
CampaignResponseStruct,
|
CampaignResponseStruct,
|
||||||
OldCampaignResponseStruct,
|
OldCampaignResponseStruct,
|
||||||
} from "../types/campaign";
|
} from "../types/campaign";
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
|
||||||
export const GetCampaignInfoHandler: AimeDBHandlerFn<"CampaignResponse" | "OldCampaignResponse"> = (
|
export const GetCampaignInfoHandler: AimeDBHandlerFn<"CampaignResponse" | "OldCampaignResponse"> = (
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PacketHeaderStruct } from "../types/header";
|
|
||||||
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
||||||
|
import { PacketHeaderStruct } from "../types/header";
|
||||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||||
|
|
||||||
export const ServiceHealthCheckHandler: AimeDBHandlerFn = (header, _) => {
|
export const ServiceHealthCheckHandler: AimeDBHandlerFn = (header, _) => {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { DFIRequestResponse } from "./middleware/dfi";
|
||||||
import type { Express } from "express";
|
import type { Express } from "express";
|
||||||
import { RequestLoggerMiddleware } from "../../lib/middleware/request-logger";
|
import { RequestLoggerMiddleware } from "../../lib/middleware/request-logger";
|
||||||
import { IsRecord } from "utils/misc";
|
import { IsRecord } from "utils/misc";
|
||||||
|
import { Config } from "lib/setup/config";
|
||||||
|
|
||||||
const logger = CreateLogCtx(__filename);
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
|
@ -54,6 +55,15 @@ app.use(RequestLoggerMiddleware);
|
||||||
|
|
||||||
app.use("/", mainRouter);
|
app.use("/", mainRouter);
|
||||||
|
|
||||||
|
if (Config.ALLNET_CONFIG.UPDATE_CFG_FOLDER) {
|
||||||
|
logger.info(`Running network delivery server at ${Config.ALLNET_CONFIG.UPDATE_CFG_FOLDER}.`, {
|
||||||
|
bootInfo: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/dl/ini", express.static(Config.ALLNET_CONFIG.UPDATE_CFG_FOLDER));
|
||||||
|
app.get("/dl/ini/*", (_, res) => res.status(404).send(""));
|
||||||
|
}
|
||||||
|
|
||||||
const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, _next) => {
|
const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, _next) => {
|
||||||
// eslint-disable-next-line cadence/no-instanceof
|
// eslint-disable-next-line cadence/no-instanceof
|
||||||
if (err instanceof SyntaxError && req.path === "/report-api/Report") {
|
if (err instanceof SyntaxError && req.path === "/report-api/Report") {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
DownloadOrderStatus,
|
DownloadOrderStatus,
|
||||||
} from "servers/allnet/types/download-order";
|
} from "servers/allnet/types/download-order";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
import type { DownloadOrderResponse } from "servers/allnet/types/download-order";
|
import type { DownloadOrderResponse } from "servers/allnet/types/download-order";
|
||||||
|
|
||||||
const logger = CreateLogCtx(__filename);
|
const logger = CreateLogCtx(__filename);
|
||||||
|
@ -14,6 +16,15 @@ const logger = CreateLogCtx(__filename);
|
||||||
const router: Router = Router({ mergeParams: true });
|
const router: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
router.post("/", async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
|
if (
|
||||||
|
!Config.ALLNET_CONFIG.UPDATE_CFG_FOLDER ||
|
||||||
|
!existsSync(Config.ALLNET_CONFIG.UPDATE_CFG_FOLDER)
|
||||||
|
) {
|
||||||
|
// Probably shouldn't place a warning.
|
||||||
|
|
||||||
|
return res.status(200).send({ stat: DownloadOrderStatus.SUCCESS, serial: "", uri: "null" });
|
||||||
|
}
|
||||||
|
|
||||||
const parseResult = DownloadOrderRequestSchema.safeParse(req.safeBody);
|
const parseResult = DownloadOrderRequestSchema.safeParse(req.safeBody);
|
||||||
|
|
||||||
if (!parseResult.success) {
|
if (!parseResult.success) {
|
||||||
|
@ -35,13 +46,31 @@ router.post("/", async (req, res) => {
|
||||||
|
|
||||||
if (!machine) {
|
if (!machine) {
|
||||||
logger.error("Rejected download order request from unknown serial.", { data });
|
logger.error("Rejected download order request from unknown serial.", { data });
|
||||||
return res.status(200).send({ stat: 1, serial: "", uri: "null" });
|
return res
|
||||||
|
.status(200)
|
||||||
|
.send({ stat: DownloadOrderStatus.SUCCESS, serial: "", uri: "null" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appIniFilename = `${data.game_id}-${data.ver.replace(/\./u, "")}-app.ini`;
|
||||||
|
const optIniFilename = `${data.game_id}-${data.ver.replace(/\./u, "")}-opt.ini`;
|
||||||
|
const appIniPath = join(Config.ALLNET_CONFIG.UPDATE_CFG_FOLDER, appIniFilename);
|
||||||
|
const optIniPath = join(Config.ALLNET_CONFIG.UPDATE_CFG_FOLDER, optIniFilename);
|
||||||
|
let uri = "";
|
||||||
|
|
||||||
|
if (existsSync(appIniPath)) {
|
||||||
|
uri = `http://${Config.ALLNET_CONFIG.HOSTNAME}/dl/ini/${appIniFilename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(optIniPath)) {
|
||||||
|
uri = `${uri}|http://${Config.ALLNET_CONFIG.HOSTNAME}/dl/ini/${optIniFilename}`;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Allow network delivery.
|
// TODO: Allow network delivery.
|
||||||
const response = {
|
const response = {
|
||||||
stat: DownloadOrderStatus.FAILURE,
|
stat: DownloadOrderStatus.SUCCESS,
|
||||||
|
serial: "",
|
||||||
|
uri,
|
||||||
} satisfies DownloadOrderResponse;
|
} satisfies DownloadOrderResponse;
|
||||||
|
|
||||||
return res.status(200).send(response);
|
return res.status(200).send(response);
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
// TODO
|
|
@ -1,20 +1,26 @@
|
||||||
import { VERSIONS } from "./versions";
|
import { VERSIONS } from "./versions";
|
||||||
|
import { ChunkLength, ToBuffer } from "../utils/buffer";
|
||||||
import { GetClassMethods } from "../utils/reflection";
|
import { GetClassMethods } from "../utils/reflection";
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import { Router, json } from "express";
|
import { Router } from "express";
|
||||||
import { ChunithmVersions } from "lib/constants/game-versions";
|
import { ChunithmVersions } from "lib/constants/game-versions";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
import { RequestLoggerMiddleware } from "lib/middleware/request-logger";
|
import { RequestLoggerMiddleware } from "lib/middleware/request-logger";
|
||||||
import { Config, Environment } from "lib/setup/config";
|
import { Config, Environment } from "lib/setup/config";
|
||||||
|
import onHeaders from "on-headers";
|
||||||
import getRawBody from "raw-body";
|
import getRawBody from "raw-body";
|
||||||
import { createDecipheriv, pbkdf2Sync } from "crypto";
|
import { IsHexadecimalString } from "utils/misc";
|
||||||
|
import { createCipheriv, createDecipheriv, pbkdf2Sync } from "crypto";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import zlib, { createDeflate, inflate } from "zlib";
|
||||||
import type { RequestHandler } from "express";
|
import type { RequestHandler } from "express";
|
||||||
|
import type { Transform } from "stream";
|
||||||
|
|
||||||
const logger = CreateLogCtx(__filename);
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
const router: Router = Router({ mergeParams: false });
|
const inflateAsync = promisify(inflate);
|
||||||
|
|
||||||
router.use(compression({ threshold: 0 }));
|
const router: Router = Router({ mergeParams: false });
|
||||||
|
|
||||||
router.use(async (req, res, next) => {
|
router.use(async (req, res, next) => {
|
||||||
if ((req.headers["content-length"] ?? 0) === 0) {
|
if ((req.headers["content-length"] ?? 0) === 0) {
|
||||||
|
@ -33,8 +39,7 @@ router.use(async (req, res, next) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEncryptedRequest =
|
const isEncryptedRequest = endpoint.length === 32 && IsHexadecimalString(endpoint);
|
||||||
endpoint.length === 32 && [...endpoint].every((c) => "0123456789abcdefABCDEF".includes(c));
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isEncryptedRequest &&
|
!isEncryptedRequest &&
|
||||||
|
@ -49,12 +54,20 @@ router.use(async (req, res, next) => {
|
||||||
return res.status(200).send({ stat: "0" });
|
return res.status(200).send({ stat: "0" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const body = await getRawBody(req, { limit: "20mb" });
|
||||||
|
|
||||||
if (!isEncryptedRequest) {
|
if (!isEncryptedRequest) {
|
||||||
|
const decompressed = await inflateAsync(body);
|
||||||
|
|
||||||
|
// eslint-disable-next-line require-atomic-updates
|
||||||
|
req.body = JSON.parse(decompressed.toString("utf-8"));
|
||||||
|
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Received encrypted request.", { version, endpoint });
|
logger.debug("Received encrypted request.", { version, endpoint });
|
||||||
|
res.locals.encrypted = true;
|
||||||
|
|
||||||
const keys = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[version];
|
const keys = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[version];
|
||||||
|
|
||||||
|
@ -74,41 +87,160 @@ router.use(async (req, res, next) => {
|
||||||
const unhashedEndpoint = userAgent.split("#")[0];
|
const unhashedEndpoint = userAgent.split("#")[0];
|
||||||
|
|
||||||
if (unhashedEndpoint) {
|
if (unhashedEndpoint) {
|
||||||
|
logger.debug("Resolved to unhashed endpoint from User-Agent.", { unhashedEndpoint });
|
||||||
req.url = req.url.replace(endpoint, unhashedEndpoint);
|
req.url = req.url.replace(endpoint, unhashedEndpoint);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"User-Agent did not contain the unhashed endpoint for version PARADISE (LOST) or older?",
|
"User-Agent did not contain the unhashed endpoint for version PARADISE (LOST) or older?",
|
||||||
{ version, userAgent }
|
{ version, userAgent }
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send({ stat: "0" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = keys[0];
|
const keyString = keys[0];
|
||||||
const iv = keys[1];
|
const ivString = keys[1];
|
||||||
|
|
||||||
if (!key || !iv) {
|
if (!keyString || !ivString) {
|
||||||
logger.error("Key or IV was not provided for the requested version.", {
|
logger.error("Key or IV was not provided for the requested version.", {
|
||||||
version,
|
version,
|
||||||
key,
|
key: keyString,
|
||||||
iv,
|
iv: ivString,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send({ stat: "0" });
|
return res.status(200).send({ stat: "0" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const cipher = createDecipheriv("aes-256-cbc", Buffer.from(key, "hex"), Buffer.from(iv, "hex"));
|
const key = Buffer.from(keyString, "hex");
|
||||||
const body = await getRawBody(req);
|
const iv = Buffer.from(ivString, "hex");
|
||||||
|
|
||||||
|
const cipher = createDecipheriv("aes-256-cbc", key, iv);
|
||||||
|
const decrypted = Buffer.concat([cipher.update(body), cipher.final()]);
|
||||||
|
const decompressed = await inflateAsync(decrypted);
|
||||||
|
|
||||||
// eslint-disable-next-line require-atomic-updates
|
// eslint-disable-next-line require-atomic-updates
|
||||||
req.body = Buffer.concat([cipher.update(body), cipher.final()]);
|
req.body = JSON.parse(decompressed.toString("utf-8"));
|
||||||
|
|
||||||
// TODO: Encrypt response
|
// Set up response necryption
|
||||||
|
// Eldritch horrors beyond my understanding down here.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const listeners: Array<[string | symbol, any]> = [];
|
||||||
|
let stream: Transform | null = null;
|
||||||
|
let ended = false;
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
const _on = res.on;
|
||||||
|
const _write = res.write;
|
||||||
|
const _end = res.end;
|
||||||
|
/* eslint-enable @typescript-eslint/unbound-method */
|
||||||
|
|
||||||
|
/* eslint-disable prefer-arrow-callback */
|
||||||
|
res.write = function write(chunk, encoding, ...rest) {
|
||||||
|
if (ended) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error monkey noises
|
||||||
|
if (!this._header && !this.headersSent) {
|
||||||
|
this.writeHead(this.statusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const enc = typeof encoding === "function" ? undefined : encoding;
|
||||||
|
|
||||||
|
return stream
|
||||||
|
? stream.write(ToBuffer(chunk, enc))
|
||||||
|
: // @ts-expect-error reeeee
|
||||||
|
_write.call(this, chunk, encoding, ...rest);
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error idk
|
||||||
|
res.end = function end(chunk, encoding, ...rest) {
|
||||||
|
if (ended) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enc = typeof encoding === "function" ? undefined : encoding;
|
||||||
|
|
||||||
|
// @ts-expect-error how do express people live like this?
|
||||||
|
if (!this._header && !this.headersSent) {
|
||||||
|
this.writeHead(this.statusCode);
|
||||||
|
|
||||||
|
// estimate the length
|
||||||
|
if (!this.getHeader("Content-Length")) {
|
||||||
|
this.setHeader("Content-Length", ChunkLength(chunk, enc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stream) {
|
||||||
|
// @ts-expect-error bruh
|
||||||
|
return _end.call(this, chunk, encoding, ...rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark ended
|
||||||
|
ended = true;
|
||||||
|
|
||||||
|
// write Buffer for Node.js 0.8
|
||||||
|
// @ts-expect-error bruh
|
||||||
|
return chunk ? stream.end(ToBuffer(chunk, encoding)) : stream.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error zzz
|
||||||
|
res.on = function on(event, listener) {
|
||||||
|
if (event !== "drain") {
|
||||||
|
return _on.call(this, event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream) {
|
||||||
|
return stream.on(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// buffer listeners for future stream
|
||||||
|
listeners.push([event, listener]);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
onHeaders(res, function onResponseHeaders() {
|
||||||
|
const cipher = createCipheriv("aes-256-cbc", key, iv);
|
||||||
|
|
||||||
|
stream = createDeflate({ flush: zlib.constants.Z_SYNC_FLUSH });
|
||||||
|
stream.pipe(cipher);
|
||||||
|
|
||||||
|
for (const listener of listeners) {
|
||||||
|
stream.on(listener[0], listener[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.removeHeader("Content-Length");
|
||||||
|
|
||||||
|
cipher.on("data", (chunk) => {
|
||||||
|
// @ts-expect-error ugggh i hate monkeys
|
||||||
|
if (!_write.call(res, chunk)) {
|
||||||
|
stream?.pause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cipher.on("end", () => {
|
||||||
|
// @ts-expect-error ugggh i hate monkeys
|
||||||
|
_end.call(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
_on.call(res, "drain", () => {
|
||||||
|
stream?.resume();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/* eslint-enable prefer-arrow-callback */
|
||||||
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// you WILL compress the response and you WILL be happy about it.
|
||||||
|
// chunithm assumes all responses are deflated.
|
||||||
|
router.use(
|
||||||
|
compression({
|
||||||
|
threshold: 0,
|
||||||
|
filter: (req, res) => !res.locals.encrypted,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
if (Environment.nodeEnv === "dev" && req.header("x-debug") !== undefined) {
|
if (Environment.nodeEnv === "dev" && req.header("x-debug") !== undefined) {
|
||||||
next();
|
next();
|
||||||
|
@ -121,9 +253,6 @@ router.use((req, res, next) => {
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// All requests to the title server will be JSON, I promise!
|
|
||||||
router.use(json({ type: (_) => true }));
|
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
// Always mount an empty req body. We operate under the assumption that req.body is
|
// Always mount an empty req body. We operate under the assumption that req.body is
|
||||||
// always defined as atleast an object.
|
// always defined as atleast an object.
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { LessThanOrEqual } from "typeorm";
|
||||||
|
import type { HasIdCardVersion } from "external/db/utils/has-id-card-version";
|
||||||
|
import type { integer } from "types/misc";
|
||||||
|
|
||||||
|
export function GetByCardIdAndVersion<T extends HasIdCardVersion>(
|
||||||
|
record: typeof HasIdCardVersion & (new (...args: Array<unknown>) => T),
|
||||||
|
cardId: integer,
|
||||||
|
version: integer
|
||||||
|
): Promise<T | null> {
|
||||||
|
return record.findOne<T>({
|
||||||
|
// @ts-expect-error urgh i hate typescript
|
||||||
|
where: {
|
||||||
|
card: {
|
||||||
|
id: cardId,
|
||||||
|
},
|
||||||
|
version: LessThanOrEqual(version),
|
||||||
|
},
|
||||||
|
// @ts-expect-error i've tried a billion ways at this point
|
||||||
|
order: {
|
||||||
|
version: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,13 +1,25 @@
|
||||||
|
import { GetByCardIdAndVersion } from "../utils";
|
||||||
|
import { ChunithmUserCharacter, ChunithmUserItem } from "external/db/entity/chunithm/item";
|
||||||
|
import {
|
||||||
|
ChunithmUserActivity,
|
||||||
|
ChunithmUserData,
|
||||||
|
ChunithmUserDataEx,
|
||||||
|
ChunithmUserOption,
|
||||||
|
ChunithmUserOptionEx,
|
||||||
|
} from "external/db/entity/chunithm/profile";
|
||||||
|
import { ChunithmUserRecentRating } from "external/db/entity/chunithm/score";
|
||||||
import { ChunithmStaticCharge, ChunithmStaticEvent } from "external/db/entity/chunithm/static";
|
import { ChunithmStaticCharge, ChunithmStaticEvent } from "external/db/entity/chunithm/static";
|
||||||
import CreateLogCtx from "lib/logger/logger";
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
import { Config } from "lib/setup/config";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import { BaseTitle } from "servers/titles/types/titles";
|
import { BaseTitle } from "servers/titles/types/titles";
|
||||||
|
import { ParseStrInt } from "servers/titles/utils/string-checks";
|
||||||
|
import { LessThanOrEqual } from "typeorm";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { ITitle } from "servers/titles/types/titles";
|
|
||||||
|
|
||||||
const logger = CreateLogCtx(__filename);
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
export class Chunithm extends BaseTitle implements ITitle {
|
export class Chunithm extends BaseTitle {
|
||||||
private readonly dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
private readonly dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
|
||||||
|
|
||||||
constructor(gameCode?: string, version?: string, servletName?: string) {
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
|
@ -78,10 +90,10 @@ export class Chunithm extends BaseTitle implements ITitle {
|
||||||
// HACK: We may want to rotate events...
|
// HACK: We may want to rotate events...
|
||||||
return res.send({
|
return res.send({
|
||||||
type: req.safeBody.type,
|
type: req.safeBody.type,
|
||||||
length: events.length.toString(),
|
length: events.length,
|
||||||
gameEventList: events.map((e) => ({
|
gameEventList: events.map((e) => ({
|
||||||
id: e.eventId.toString(),
|
id: e.eventId,
|
||||||
type: e.type.toString(),
|
type: e.type,
|
||||||
startDate: DateTime.fromJSDate(e.startDate).toFormat(this.dateTimeFormat),
|
startDate: DateTime.fromJSDate(e.startDate).toFormat(this.dateTimeFormat),
|
||||||
endDate: "2099-12-31 00:00:00",
|
endDate: "2099-12-31 00:00:00",
|
||||||
})),
|
})),
|
||||||
|
@ -89,7 +101,7 @@ export class Chunithm extends BaseTitle implements ITitle {
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetGameRankingApi(req: Request, res: Response) {
|
handle_GetGameRankingApi(req: Request, res: Response) {
|
||||||
// TODO
|
// TODO: Get most played songs from playlog
|
||||||
return res.send({
|
return res.send({
|
||||||
type: req.safeBody.type,
|
type: req.safeBody.type,
|
||||||
length: "0",
|
length: "0",
|
||||||
|
@ -114,10 +126,10 @@ export class Chunithm extends BaseTitle implements ITitle {
|
||||||
|
|
||||||
// HACK
|
// HACK
|
||||||
return res.send({
|
return res.send({
|
||||||
length: charges.length.toString(),
|
length: charges.length,
|
||||||
gameChargeList: charges.map((c, i) => ({
|
gameChargeList: charges.map((c, i) => ({
|
||||||
orderId: i.toString(),
|
orderId: i,
|
||||||
chargeId: c.chargeId.toString(),
|
chargeId: c.chargeId,
|
||||||
price: "1",
|
price: "1",
|
||||||
startDate: "2017-12-05 07:00:00.0",
|
startDate: "2017-12-05 07:00:00.0",
|
||||||
endDate: "2099-12-31 00:00:00.0",
|
endDate: "2099-12-31 00:00:00.0",
|
||||||
|
@ -128,36 +140,350 @@ export class Chunithm extends BaseTitle implements ITitle {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserPreviewApi(req: Request, res: Response) {
|
async handle_GetUserPreviewApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.error("Invalid request: received userId is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await GetByCardIdAndVersion(ChunithmUserData, userId, this.numericVersion);
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
logger.error(`No users found.`, {
|
||||||
|
userId,
|
||||||
|
version: this.numericVersion,
|
||||||
|
});
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = await GetByCardIdAndVersion(
|
||||||
|
ChunithmUserOption,
|
||||||
|
userId,
|
||||||
|
this.numericVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
const character = await ChunithmUserCharacter.findOne({
|
||||||
|
where: {
|
||||||
|
characterId: profile.characterId,
|
||||||
|
card: {
|
||||||
|
id: profile.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastPlayDate = DateTime.fromJSDate(profile.lastPlayDate ?? new Date(0)).toFormat(
|
||||||
|
this.dateTimeFormat
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
|
||||||
|
// TODO: This should probably be false if the user is having a session somewhere else.
|
||||||
|
isLogin: false,
|
||||||
|
|
||||||
|
lastLoginDate: lastPlayDate,
|
||||||
|
userName: profile.userName,
|
||||||
|
reincarnationNum: profile.reincarnationNum,
|
||||||
|
level: profile.level,
|
||||||
|
exp: profile.exp,
|
||||||
|
playerRating: profile.playerRating,
|
||||||
|
lastGameId: profile.lastGameId,
|
||||||
|
lastRomVersion: profile.lastRomVersion,
|
||||||
|
lastDataVersion: profile.lastDataVersion,
|
||||||
|
lastPlayDate,
|
||||||
|
trophyId: profile.trophyId,
|
||||||
|
nameplateId: profile.nameplateId,
|
||||||
|
playerLevel: profile.level,
|
||||||
|
rating: profile.playerRating,
|
||||||
|
headphone: options?.headphone ?? 0,
|
||||||
|
chargeState: 1,
|
||||||
|
userNameEx: profile.userNameEx,
|
||||||
|
userCharacter: character ? { ...character, id: undefined, profile: undefined } : {},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GameLoginApi(req: Request, res: Response) {
|
handle_GameLoginApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
if (!Config.CHUNITHM_CONFIG.MODS.USE_LOGIN_BONUS) {
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add login bonuses
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserDataApi(req: Request, res: Response) {
|
async handle_GetUserDataApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.error("Invalid request: received userId is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await GetByCardIdAndVersion(ChunithmUserData, userId, this.numericVersion);
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
logger.error(`No users found.`, {
|
||||||
|
userId,
|
||||||
|
version: this.numericVersion,
|
||||||
|
});
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
userData: { ...profile, id: undefined, card: undefined, version: undefined },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserOptionApi(req: Request, res: Response) {
|
async handle_GetUserDataExApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.error("Invalid request: received userId is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await GetByCardIdAndVersion(
|
||||||
|
ChunithmUserDataEx,
|
||||||
|
userId,
|
||||||
|
this.numericVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!profile) {
|
||||||
|
logger.error(`No users found.`, {
|
||||||
|
userId,
|
||||||
|
version: this.numericVersion,
|
||||||
|
});
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
userDataEx: { ...profile, id: undefined, card: undefined, version: undefined },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserCharacterApi(req: Request, res: Response) {
|
async handle_GetUserOptionApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.error("Invalid request: received userId is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = await GetByCardIdAndVersion(
|
||||||
|
ChunithmUserOption,
|
||||||
|
userId,
|
||||||
|
this.numericVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
logger.error(`No options found for requested user.`, {
|
||||||
|
userId,
|
||||||
|
version: this.numericVersion,
|
||||||
|
});
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
userGameOption: { ...options, id: undefined, card: undefined, version: undefined },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserActivityApi(req: Request, res: Response) {
|
async handle_GetUserOptionExApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.error("Invalid request: received userId is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = await GetByCardIdAndVersion(
|
||||||
|
ChunithmUserOptionEx,
|
||||||
|
userId,
|
||||||
|
this.numericVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
logger.error(`No options found for requested user.`, {
|
||||||
|
userId,
|
||||||
|
version: this.numericVersion,
|
||||||
|
});
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
userGameOption: { ...options, id: undefined, card: undefined, version: undefined },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserItemApi(req: Request, res: Response) {
|
async handle_GetUserCharacterApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
const nextIndex = ParseStrInt(req.safeBody.nextIndex);
|
||||||
|
const maxCount = ParseStrInt(req.safeBody.maxCount);
|
||||||
|
|
||||||
|
if (!userId || !nextIndex || !maxCount) {
|
||||||
|
logger.error("Invalid request: received userId/nextIndex/maxCount is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
nextIndex: req.safeBody.nextIndex,
|
||||||
|
maxCount: req.safeBody.maxCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const [characters, characterCount] = await ChunithmUserCharacter.findAndCount({
|
||||||
|
where: {
|
||||||
|
card: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
take: maxCount,
|
||||||
|
skip: nextIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextNextIndex = nextIndex + maxCount;
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
length: characters.length,
|
||||||
|
nextIndex: nextNextIndex < characterCount ? nextNextIndex : -1,
|
||||||
|
userCharacterList: characters.map((c) => ({ ...c, id: undefined, card: undefined })),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserRecentRatingApi(req: Request, res: Response) {
|
async handle_GetUserActivityApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
const kind = ParseStrInt(req.safeBody.kind);
|
||||||
|
|
||||||
|
if (!userId || !kind) {
|
||||||
|
logger.error("Invalid request: received userId/kind is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
kind: req.safeBody.kind,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const activities = await ChunithmUserActivity.find({
|
||||||
|
where: {
|
||||||
|
card: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
kind,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
length: activities.length,
|
||||||
|
kind,
|
||||||
|
userActivityList: activities.map((a) => ({
|
||||||
|
...a,
|
||||||
|
id: a.activityId,
|
||||||
|
activityId: undefined,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle_GetUserItemApi(req: Request, res: Response) {
|
||||||
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
const nextIndex = ParseStrInt(req.safeBody.nextIndex);
|
||||||
|
const maxCount = ParseStrInt(req.safeBody.maxCount);
|
||||||
|
|
||||||
|
if (!userId || !nextIndex || !maxCount) {
|
||||||
|
logger.error("Invalid request: received userId/nextIndex/maxCount is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
nextIndex: req.safeBody.nextIndex,
|
||||||
|
maxCount: req.safeBody.maxCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemKind = Math.trunc(nextIndex / 1e10);
|
||||||
|
const skip = nextIndex % 1e10;
|
||||||
|
|
||||||
|
const [items, itemsCount] = await ChunithmUserItem.findAndCount({
|
||||||
|
where: {
|
||||||
|
card: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
itemKind,
|
||||||
|
},
|
||||||
|
take: maxCount,
|
||||||
|
skip,
|
||||||
|
});
|
||||||
|
|
||||||
|
const nextIndexWithinKind = skip + maxCount;
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
nextIndex: nextIndexWithinKind < itemsCount ? itemKind * 1e10 + nextIndexWithinKind : 0,
|
||||||
|
itemKind,
|
||||||
|
length: items.length,
|
||||||
|
userItemList: items.map((i) => ({ ...i, id: undefined, card: undefined })),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async handle_GetUserRecentRatingApi(req: Request, res: Response) {
|
||||||
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
logger.error("Invalid request: received userId is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentRatings = await ChunithmUserRecentRating.find({
|
||||||
|
where: {
|
||||||
|
card: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
version: LessThanOrEqual(this.numericVersion),
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
order: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxVersion = Math.max(...recentRatings.map((r) => r.version));
|
||||||
|
const userRecentRatingList = recentRatings
|
||||||
|
.filter((r) => r.version === maxVersion)
|
||||||
|
.map((r) => ({
|
||||||
|
...r,
|
||||||
|
id: undefined,
|
||||||
|
card: undefined,
|
||||||
|
version: undefined,
|
||||||
|
order: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
length: userRecentRatingList.length,
|
||||||
|
userRecentRatingList,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserMusicApi(req: Request, res: Response) {
|
handle_GetUserMusicApi(req: Request, res: Response) {
|
||||||
|
@ -165,11 +491,12 @@ export class Chunithm extends BaseTitle implements ITitle {
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserRegionApi(req: Request, res: Response) {
|
handle_GetUserRegionApi(req: Request, res: Response) {
|
||||||
throw new Error("Unimplemented");
|
// TODO: What the hell is a region anyways?
|
||||||
}
|
return res.send({
|
||||||
|
userId: req.safeBody.userId,
|
||||||
handle_GetUserFavoriteItemApi(req: Request, res: Response) {
|
length: 0,
|
||||||
throw new Error("Unimplemented");
|
userRegionList: [],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_GetUserLoginBonusApi(req: Request, res: Response) {
|
handle_GetUserLoginBonusApi(req: Request, res: Response) {
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { ChunithmAmazon } from "./130-amazon";
|
import { ChunithmAmazon } from "./130-amazon";
|
||||||
|
import { ChunithmUserFavoriteItem } from "external/db/entity/chunithm/item";
|
||||||
|
import CreateLogCtx from "lib/logger/logger";
|
||||||
|
import { ParseStrInt } from "servers/titles/utils/string-checks";
|
||||||
|
import { LessThanOrEqual } from "typeorm";
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
|
|
||||||
|
const logger = CreateLogCtx(__filename);
|
||||||
|
|
||||||
export class ChunithmAmazonPlus extends ChunithmAmazon {
|
export class ChunithmAmazonPlus extends ChunithmAmazon {
|
||||||
constructor(gameCode?: string, version?: string, servletName?: string) {
|
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||||
super(gameCode ?? "SDBT", version ?? "135", servletName ?? "ChuniServlet");
|
super(gameCode ?? "SDBT", version ?? "135", servletName ?? "ChuniServlet");
|
||||||
|
@ -26,4 +32,41 @@ export class ChunithmAmazonPlus extends ChunithmAmazon {
|
||||||
userFavoriteMusicList: [],
|
userFavoriteMusicList: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handle_GetUserFavoriteItemApi(req: Request, res: Response) {
|
||||||
|
const userId = ParseStrInt(req.safeBody.userId);
|
||||||
|
const kind = ParseStrInt(req.safeBody.kind);
|
||||||
|
|
||||||
|
if (!userId || !kind) {
|
||||||
|
logger.error("Invalid request: received userId/kind is not a number.", {
|
||||||
|
userId: req.safeBody.userId,
|
||||||
|
kind: req.safeBody.kind,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.send({ returnCode: "1" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const favorites = await ChunithmUserFavoriteItem.find({
|
||||||
|
where: {
|
||||||
|
card: {
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
version: LessThanOrEqual(this.numericVersion),
|
||||||
|
favKind: kind,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxVersion = Math.max(...favorites.map((f) => f.version));
|
||||||
|
const userFavoriteItemList = favorites
|
||||||
|
.filter((f) => f.version === maxVersion)
|
||||||
|
.map((f) => ({ id: f.favId }));
|
||||||
|
|
||||||
|
return res.send({
|
||||||
|
userId,
|
||||||
|
length: userFavoriteItemList.length,
|
||||||
|
kind,
|
||||||
|
nextIndex: -1,
|
||||||
|
userFavoriteItemList,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
function IsNumericArray(a: unknown): a is Array<number> {
|
||||||
|
return Array.isArray(a) && a.every((c) => typeof c === "number");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ToBuffer(chunk: unknown, encoding?: BufferEncoding) {
|
||||||
|
if (Buffer.isBuffer(chunk)) {
|
||||||
|
return chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsNumericArray(chunk)) {
|
||||||
|
return Buffer.from(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof chunk === "string") {
|
||||||
|
return Buffer.from(chunk, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Cannot transform into a buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChunkLength(chunk: unknown, encoding?: BufferEncoding) {
|
||||||
|
if (Buffer.isBuffer(chunk)) {
|
||||||
|
return chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line cadence/no-instanceof
|
||||||
|
if (chunk instanceof DataView) {
|
||||||
|
return Buffer.byteLength(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof chunk === "string") {
|
||||||
|
return Buffer.byteLength(chunk, encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Cannot transform into a buffer");
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
const isIntegerRegex = /^-?\d+$/u;
|
||||||
|
|
||||||
|
export function ParseStrInt(val: unknown): number | null {
|
||||||
|
if (typeof val !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isIntegerRegex.test(val)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = Number(val);
|
||||||
|
|
||||||
|
if (!Number.isSafeInteger(v)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
|
@ -11,3 +11,7 @@ export function EscapeStringRegexp(string: string) {
|
||||||
export function IsRecord(maybeRecord: unknown): maybeRecord is Record<string, unknown> {
|
export function IsRecord(maybeRecord: unknown): maybeRecord is Record<string, unknown> {
|
||||||
return typeof maybeRecord === "object" && maybeRecord !== null;
|
return typeof maybeRecord === "object" && maybeRecord !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function IsHexadecimalString(s: string): boolean {
|
||||||
|
return /^[0-9a-f]+$/iu.test(s);
|
||||||
|
}
|
||||||
|
|
|
@ -106,5 +106,10 @@
|
||||||
/* Completeness */
|
/* Completeness */
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"require": [
|
||||||
|
"tsconfig-paths/register"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue