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
|
||||
|
||||
data
|
||||
logs
|
||||
*.sqlite3
|
||||
*.sqlite3-shm
|
||||
*.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",
|
||||
"express": "^4.18.2",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"fletcher": "^0.0.3",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"json5": "^2.2.3",
|
||||
"luxon": "^3.4.4",
|
||||
"micro-packed": "^0.3.2",
|
||||
"on-headers": "^1.0.2",
|
||||
"raw-body": "^2.5.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"safe-json-stringify": "^1.2.0",
|
||||
|
@ -50,6 +49,7 @@
|
|||
"@types/iconv": "^3.0.4",
|
||||
"@types/luxon": "^3.3.4",
|
||||
"@types/node": "16",
|
||||
"@types/on-headers": "^1.0.3",
|
||||
"@types/safe-json-stringify": "^1.1.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||
"@typescript-eslint/parser": "5.47.1",
|
||||
|
|
|
@ -7,6 +7,7 @@ specifiers:
|
|||
'@types/iconv': ^3.0.4
|
||||
'@types/luxon': ^3.3.4
|
||||
'@types/node': '16'
|
||||
'@types/on-headers': ^1.0.3
|
||||
'@types/safe-json-stringify': ^1.1.5
|
||||
'@typescript-eslint/eslint-plugin': 5.47.1
|
||||
'@typescript-eslint/parser': 5.47.1
|
||||
|
@ -21,10 +22,12 @@ specifiers:
|
|||
json5: ^2.2.3
|
||||
luxon: ^3.4.4
|
||||
micro-packed: ^0.3.2
|
||||
on-headers: ^1.0.2
|
||||
raw-body: ^2.5.2
|
||||
reflect-metadata: ^0.1.13
|
||||
safe-json-stringify: ^1.2.0
|
||||
sqlite3: ^5.1.6
|
||||
stream-combiner: ^0.2.2
|
||||
tap: ^18.6.1
|
||||
tsconfig-paths: ^4.2.0
|
||||
typed-struct: ^2.3.0
|
||||
|
@ -45,10 +48,12 @@ dependencies:
|
|||
json5: 2.2.3
|
||||
luxon: 3.4.4
|
||||
micro-packed: 0.3.2
|
||||
on-headers: 1.0.2
|
||||
raw-body: 2.5.2
|
||||
reflect-metadata: 0.1.13
|
||||
safe-json-stringify: 1.2.0
|
||||
sqlite3: 5.1.6
|
||||
stream-combiner: 0.2.2
|
||||
tsconfig-paths: 4.2.0
|
||||
typed-struct: 2.3.0_iconv-lite@0.6.3
|
||||
typeorm: 0.3.17_sqlite3@5.1.6
|
||||
|
@ -64,6 +69,7 @@ devDependencies:
|
|||
'@types/iconv': 3.0.4
|
||||
'@types/luxon': 3.3.4
|
||||
'@types/node': 16.18.62
|
||||
'@types/on-headers': 1.0.3
|
||||
'@types/safe-json-stringify': 1.1.5
|
||||
'@typescript-eslint/eslint-plugin': 5.47.1_o6yrxajvsx2b7l3udqdd2yq4ii
|
||||
'@typescript-eslint/parser': 5.47.1_4njjt2tu6ubn7rwbgcnueihm54
|
||||
|
@ -942,6 +948,12 @@ packages:
|
|||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==}
|
||||
dev: true
|
||||
|
@ -1825,6 +1837,10 @@ packages:
|
|||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/duplexer/0.1.2:
|
||||
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
|
||||
dev: false
|
||||
|
||||
/eastasianwidth/0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
dev: true
|
||||
|
@ -4560,6 +4576,13 @@ packages:
|
|||
engines: {node: '>= 0.8'}
|
||||
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:
|
||||
resolution: {integrity: sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==}
|
||||
engines: {node: '>=16'}
|
||||
|
@ -4792,6 +4815,10 @@ packages:
|
|||
any-promise: 1.3.0
|
||||
dev: false
|
||||
|
||||
/through/2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
dev: false
|
||||
|
||||
/to-regex-range/5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
|
|
@ -1,26 +1,14 @@
|
|||
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 { DataSource } from "typeorm";
|
||||
import { join } from "path";
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: "sqlite",
|
||||
database: Config.DATABASE_PATH,
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
entities: [
|
||||
Arcade,
|
||||
AimeCard,
|
||||
AimeUser,
|
||||
ChunithmStaticEvent,
|
||||
EventLog,
|
||||
FelicaCardLookup,
|
||||
FelicaMobileLookup,
|
||||
Machine,
|
||||
],
|
||||
entities: [join(__dirname, "entity", "**", "*.{ts,js}")],
|
||||
migrations: [],
|
||||
subscribers: [],
|
||||
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),
|
||||
FOLDER: z.string().default("logs"),
|
||||
}),
|
||||
ALLNET_CONFIG: z.object({
|
||||
ENABLED: z.boolean().default(true),
|
||||
PORT: zod16bitNumber.default(80),
|
||||
ALLOW_UNREGISTERED_SERIALS: z.boolean().default(true),
|
||||
UPDATE_CFG_FOLDER: z.string().optional(),
|
||||
}),
|
||||
ALLNET_CONFIG: z
|
||||
.object({
|
||||
ENABLED: z.boolean().default(true),
|
||||
PORT: zod16bitNumber.default(80),
|
||||
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({
|
||||
ENABLED: z.boolean().default(true),
|
||||
PORT: zod16bitNumber.default(22345),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CommandId, PortalRegistration, ResultCodes } from "../../../lib/constants/aimedb";
|
||||
import {
|
||||
AimeAccountQueryStruct,
|
||||
AimeAccountResponseStruct,
|
||||
|
@ -5,7 +6,6 @@ import {
|
|||
AimeAccountExtendedQueryStruct,
|
||||
} from "../types/aime-account";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, PortalRegistration, ResultCodes } from "../../../lib/constants/aimedb";
|
||||
import { AimeCard } from "external/db/entity/aimedb";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
@ -35,6 +35,7 @@ export const GetAimeAccountHandler: AimeDBHandlerFn<"AimeAccountResponse"> = asy
|
|||
return resp;
|
||||
}
|
||||
|
||||
// TODO: Verify access code validity
|
||||
const accessCode = Buffer.from(req.accessCode).toString("hex");
|
||||
|
||||
const card = await AimeCard.findOne({
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { CommandId, LogStatus, ResultCodes } from "../../../lib/constants/aimedb";
|
||||
import {
|
||||
AimeLogStruct,
|
||||
AimeLogExtendedResponseStruct,
|
||||
|
@ -5,7 +6,6 @@ import {
|
|||
StatusLogStruct,
|
||||
} from "../types/aime-log";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, LogStatus, ResultCodes } from "../../../lib/constants/aimedb";
|
||||
import { AppDataSource } from "external/db/data-source";
|
||||
import { EventLog } from "external/db/entity/base";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
// TODO: Actually support campaigns
|
||||
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
||||
import {
|
||||
CampaignClearInfoResponseStruct,
|
||||
CampaignResponseStruct,
|
||||
OldCampaignResponseStruct,
|
||||
} from "../types/campaign";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
||||
export const GetCampaignInfoHandler: AimeDBHandlerFn<"CampaignResponse" | "OldCampaignResponse"> = (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { PacketHeaderStruct } from "../types/header";
|
||||
import { CommandId, ResultCodes } from "../../../lib/constants/aimedb";
|
||||
import { PacketHeaderStruct } from "../types/header";
|
||||
import type { AimeDBHandlerFn } from "../types/handlers";
|
||||
|
||||
export const ServiceHealthCheckHandler: AimeDBHandlerFn = (header, _) => {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { DFIRequestResponse } from "./middleware/dfi";
|
|||
import type { Express } from "express";
|
||||
import { RequestLoggerMiddleware } from "../../lib/middleware/request-logger";
|
||||
import { IsRecord } from "utils/misc";
|
||||
import { Config } from "lib/setup/config";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
|
@ -54,6 +55,15 @@ app.use(RequestLoggerMiddleware);
|
|||
|
||||
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) => {
|
||||
// eslint-disable-next-line cadence/no-instanceof
|
||||
if (err instanceof SyntaxError && req.path === "/report-api/Report") {
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
DownloadOrderStatus,
|
||||
} from "servers/allnet/types/download-order";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import type { DownloadOrderResponse } from "servers/allnet/types/download-order";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
@ -14,6 +16,15 @@ const logger = CreateLogCtx(__filename);
|
|||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
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);
|
||||
|
||||
if (!parseResult.success) {
|
||||
|
@ -35,13 +46,31 @@ router.post("/", async (req, res) => {
|
|||
|
||||
if (!machine) {
|
||||
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.
|
||||
const response = {
|
||||
stat: DownloadOrderStatus.FAILURE,
|
||||
stat: DownloadOrderStatus.SUCCESS,
|
||||
serial: "",
|
||||
uri,
|
||||
} satisfies DownloadOrderResponse;
|
||||
|
||||
return res.status(200).send(response);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
// TODO
|
|
@ -1,20 +1,26 @@
|
|||
import { VERSIONS } from "./versions";
|
||||
import { ChunkLength, ToBuffer } from "../utils/buffer";
|
||||
import { GetClassMethods } from "../utils/reflection";
|
||||
import compression from "compression";
|
||||
import { Router, json } from "express";
|
||||
import { Router } from "express";
|
||||
import { ChunithmVersions } from "lib/constants/game-versions";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { RequestLoggerMiddleware } from "lib/middleware/request-logger";
|
||||
import { Config, Environment } from "lib/setup/config";
|
||||
import onHeaders from "on-headers";
|
||||
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 { Transform } from "stream";
|
||||
|
||||
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) => {
|
||||
if ((req.headers["content-length"] ?? 0) === 0) {
|
||||
|
@ -33,8 +39,7 @@ router.use(async (req, res, next) => {
|
|||
return;
|
||||
}
|
||||
|
||||
const isEncryptedRequest =
|
||||
endpoint.length === 32 && [...endpoint].every((c) => "0123456789abcdefABCDEF".includes(c));
|
||||
const isEncryptedRequest = endpoint.length === 32 && IsHexadecimalString(endpoint);
|
||||
|
||||
if (
|
||||
!isEncryptedRequest &&
|
||||
|
@ -49,12 +54,20 @@ router.use(async (req, res, next) => {
|
|||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
|
||||
const body = await getRawBody(req, { limit: "20mb" });
|
||||
|
||||
if (!isEncryptedRequest) {
|
||||
const decompressed = await inflateAsync(body);
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
req.body = JSON.parse(decompressed.toString("utf-8"));
|
||||
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Received encrypted request.", { version, endpoint });
|
||||
res.locals.encrypted = true;
|
||||
|
||||
const keys = Config.CHUNITHM_CONFIG.CRYPTO.KEYS?.[version];
|
||||
|
||||
|
@ -74,41 +87,160 @@ router.use(async (req, res, next) => {
|
|||
const unhashedEndpoint = userAgent.split("#")[0];
|
||||
|
||||
if (unhashedEndpoint) {
|
||||
logger.debug("Resolved to unhashed endpoint from User-Agent.", { unhashedEndpoint });
|
||||
req.url = req.url.replace(endpoint, unhashedEndpoint);
|
||||
} else {
|
||||
logger.warn(
|
||||
"User-Agent did not contain the unhashed endpoint for version PARADISE (LOST) or older?",
|
||||
{ version, userAgent }
|
||||
);
|
||||
|
||||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
}
|
||||
|
||||
const key = keys[0];
|
||||
const iv = keys[1];
|
||||
const keyString = keys[0];
|
||||
const ivString = keys[1];
|
||||
|
||||
if (!key || !iv) {
|
||||
if (!keyString || !ivString) {
|
||||
logger.error("Key or IV was not provided for the requested version.", {
|
||||
version,
|
||||
key,
|
||||
iv,
|
||||
key: keyString,
|
||||
iv: ivString,
|
||||
});
|
||||
|
||||
return res.status(200).send({ stat: "0" });
|
||||
}
|
||||
|
||||
const cipher = createDecipheriv("aes-256-cbc", Buffer.from(key, "hex"), Buffer.from(iv, "hex"));
|
||||
const body = await getRawBody(req);
|
||||
const key = Buffer.from(keyString, "hex");
|
||||
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
|
||||
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();
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
if (Environment.nodeEnv === "dev" && req.header("x-debug") !== undefined) {
|
||||
next();
|
||||
|
@ -121,9 +253,6 @@ router.use((req, res, next) => {
|
|||
next();
|
||||
});
|
||||
|
||||
// All requests to the title server will be JSON, I promise!
|
||||
router.use(json({ type: (_) => true }));
|
||||
|
||||
router.use((req, res, next) => {
|
||||
// Always mount an empty req body. We operate under the assumption that req.body is
|
||||
// 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 CreateLogCtx from "lib/logger/logger";
|
||||
import { Config } from "lib/setup/config";
|
||||
import { DateTime } from "luxon";
|
||||
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 { ITitle } from "servers/titles/types/titles";
|
||||
|
||||
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";
|
||||
|
||||
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...
|
||||
return res.send({
|
||||
type: req.safeBody.type,
|
||||
length: events.length.toString(),
|
||||
length: events.length,
|
||||
gameEventList: events.map((e) => ({
|
||||
id: e.eventId.toString(),
|
||||
type: e.type.toString(),
|
||||
id: e.eventId,
|
||||
type: e.type,
|
||||
startDate: DateTime.fromJSDate(e.startDate).toFormat(this.dateTimeFormat),
|
||||
endDate: "2099-12-31 00:00:00",
|
||||
})),
|
||||
|
@ -89,7 +101,7 @@ export class Chunithm extends BaseTitle implements ITitle {
|
|||
}
|
||||
|
||||
handle_GetGameRankingApi(req: Request, res: Response) {
|
||||
// TODO
|
||||
// TODO: Get most played songs from playlog
|
||||
return res.send({
|
||||
type: req.safeBody.type,
|
||||
length: "0",
|
||||
|
@ -114,10 +126,10 @@ export class Chunithm extends BaseTitle implements ITitle {
|
|||
|
||||
// HACK
|
||||
return res.send({
|
||||
length: charges.length.toString(),
|
||||
length: charges.length,
|
||||
gameChargeList: charges.map((c, i) => ({
|
||||
orderId: i.toString(),
|
||||
chargeId: c.chargeId.toString(),
|
||||
orderId: i,
|
||||
chargeId: c.chargeId,
|
||||
price: "1",
|
||||
startDate: "2017-12-05 07: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) {
|
||||
throw new Error("Unimplemented");
|
||||
async handle_GetUserPreviewApi(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 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) {
|
||||
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) {
|
||||
throw new Error("Unimplemented");
|
||||
async handle_GetUserDataApi(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 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) {
|
||||
throw new Error("Unimplemented");
|
||||
async handle_GetUserDataExApi(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 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) {
|
||||
throw new Error("Unimplemented");
|
||||
async handle_GetUserOptionApi(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 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) {
|
||||
throw new Error("Unimplemented");
|
||||
async handle_GetUserOptionExApi(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 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) {
|
||||
throw new Error("Unimplemented");
|
||||
async handle_GetUserCharacterApi(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 [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) {
|
||||
throw new Error("Unimplemented");
|
||||
async handle_GetUserActivityApi(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 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) {
|
||||
|
@ -165,11 +491,12 @@ export class Chunithm extends BaseTitle implements ITitle {
|
|||
}
|
||||
|
||||
handle_GetUserRegionApi(req: Request, res: Response) {
|
||||
throw new Error("Unimplemented");
|
||||
}
|
||||
|
||||
handle_GetUserFavoriteItemApi(req: Request, res: Response) {
|
||||
throw new Error("Unimplemented");
|
||||
// TODO: What the hell is a region anyways?
|
||||
return res.send({
|
||||
userId: req.safeBody.userId,
|
||||
length: 0,
|
||||
userRegionList: [],
|
||||
});
|
||||
}
|
||||
|
||||
handle_GetUserLoginBonusApi(req: Request, res: Response) {
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
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";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export class ChunithmAmazonPlus extends ChunithmAmazon {
|
||||
constructor(gameCode?: string, version?: string, servletName?: string) {
|
||||
super(gameCode ?? "SDBT", version ?? "135", servletName ?? "ChuniServlet");
|
||||
|
@ -26,4 +32,41 @@ export class ChunithmAmazonPlus extends ChunithmAmazon {
|
|||
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> {
|
||||
return typeof maybeRecord === "object" && maybeRecord !== null;
|
||||
}
|
||||
|
||||
export function IsHexadecimalString(s: string): boolean {
|
||||
return /^[0-9a-f]+$/iu.test(s);
|
||||
}
|
||||
|
|
|
@ -106,5 +106,10 @@
|
|||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"ts-node": {
|
||||
"require": [
|
||||
"tsconfig-paths/register"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue