diff --git a/schema/init/cm.sql b/schema/init/cm.sql new file mode 100644 index 0000000..715d0d9 --- /dev/null +++ b/schema/init/cm.sql @@ -0,0 +1,304 @@ +-- SQLite's ALTER TABLE statement is almost useless, and who knows how much of +-- this stuff is present in older versions. So while ideally we'd mark a lot +-- of these columns NOT NULL that might come back to bite us as we extend +-- support downward from Chunithm 4 to Chunithm 1. + +create table "cm_user_data" ( + "id" integer primary key not null, + -- Could in theory do a shared primary key with aime_player, since this + -- is going to be a 1:1 relation for the foreseeable future, but again + -- the SQLite strait-jacket forces us to design defensively. + "player_id" integer not null + references "aime_player"("id") + on delete cascade, + "access_code" text, + "user_name" text, + "is_web_join" text, + "web_limit_date" text, + "level" integer, + "reincarnation_num" integer, + "exp" integer, + "point" integer, + "total_point" integer, + "play_count" integer, + "multi_play_count" integer, + "multi_win_count" integer, + "request_res_count" integer, + "accept_res_count" integer, + "success_res_count" integer, + "player_rating" integer, + "highest_rating" integer, + "nameplate_id" integer, + "frame_id" integer, + "character_id" integer, + "trophy_id" integer, + "played_tutorial_bit" integer, + "first_tutorial_cancel_num" integer, + "master_tutorial_cancel_num" integer, + "total_repertoire_count" integer, + "total_map_num" integer, + "total_hi_score" integer, + "total_basic_high_score" integer, + "total_advanced_high_score" integer, + "total_expert_high_score" integer, + "total_master_high_score" integer, + "event_watched_date" text, + "friend_count" integer, + "is_maimai" text, + "first_game_id" text, + "first_rom_version" text, + "first_data_version" text, + "first_play_date" text, + "last_game_id" text, + "last_rom_version" text, + "last_data_version" text, + "last_play_date" text, + "last_place_id" integer, + "last_place_name" text, + "last_region_id" text, + "last_region_name" text, + "last_all_net_id" text, + "last_client_id" text +); + +create table "cm_user_activity" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "kind" integer not null, + -- This is just called "id" on the wire but, well, we already have an "id" + -- column up there ^ + "activity_id" integer not null, + "sort_number" integer not null, + "param1" integer not null, + "param2" integer not null, + "param3" integer not null, + "param4" integer not null, + constraint "cm_user_activity_uq" unique ( + "profile_id", + "kind", + "activity_id" + ) +); + +create table "cm_user_data_ex" ( + "id" integer primary key not null + references "cm_user_data"("id") + on delete cascade, + "compatible_cm_version" text not null, + "medal" integer not null, + "map_icon_id" integer not null, + "voice_id" integer not null, + "ext1" integer not null, + "ext2" integer not null, + "ext3" integer not null, + "ext4" integer not null, + "ext5" integer not null, + "ext6" integer not null, + "ext7" integer not null, + "ext8" integer not null, + "ext9" integer not null, + "ext10" integer not null, + "ext11" integer not null, + "ext12" integer not null, + "ext13" integer not null, + "ext14" integer not null, + "ext15" integer not null, + "ext16" integer not null, + "ext17" integer not null, + "ext18" integer not null, + "ext19" integer not null, + "ext20" integer not null, + "ext_str1" text not null, + "ext_str2" text not null, + "ext_str3" text not null, + "ext_str4" text not null, + "ext_str5" text not null, + "ext_long1" integer not null, + "ext_long2" integer not null, + "ext_long3" integer not null, + "ext_long4" integer not null, + "ext_long5" integer not null +); + +create table "cm_user_character" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "character_id" integer not null, + "play_count" integer not null, + "level" integer not null, + "skill_id" integer not null, + "friendship_exp" integer not null, + "is_valid" integer not null, + "is_new_mark" integer not null, + "param1" integer not null, + "param2" integer not null, + constraint "cm_user_character_uq" unique ("profile_id", "character_id") +); + +create table "cm_user_game_option" ( + "id" integer primary key not null + references "cm_user_data"("id") + on delete cascade, + "bg_info" integer not null, + "field_color" integer not null, + "guide_sound" integer not null, + "sound_effect" integer not null, + "guide_line" integer not null, + "speed" integer not null, + "option_set" integer not null, + "matching" integer not null, + "judge_pos" integer not null, + "rating" integer not null, + "judge_justice" integer not null, + "judge_attack" integer not null, + "headphone" integer not null, + "player_level" integer not null, + "success_tap" integer not null, + "success_ex_tap" integer not null, + "success_slide_hold" integer not null, + "success_air" integer not null, + "success_flick" integer not null, + "success_skill" integer not null, + "success_tap_timbre" integer not null, + "privacy" integer not null +); + +create table "cm_user_game_option_ex" ( + "id" integer primary key not null + references "cm_user_data"("id") + on delete cascade, + "ext1" integer not null, + "ext2" integer not null, + "ext3" integer not null, + "ext4" integer not null, + "ext5" integer not null, + "ext6" integer not null, + "ext7" integer not null, + "ext8" integer not null, + "ext9" integer not null, + "ext10" integer not null, + "ext11" integer not null, + "ext12" integer not null, + "ext13" integer not null, + "ext14" integer not null, + "ext15" integer not null, + "ext16" integer not null, + "ext17" integer not null, + "ext18" integer not null, + "ext19" integer not null, + "ext20" integer not null +); + +create table "cm_user_item" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "item_kind" integer not null, + "item_id" integer not null, + "stock" integer not null, + "is_valid" text not null, + constraint "cm_user_item_uq" unique ("profile_id", "item_kind", "item_id") +); + +create table "cm_user_map" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "map_id" integer not null, + "position" integer not null, + "is_clear" text not null, + "area_id" integer not null, + "route_number" integer not null, + "event_id" integer not null, + "rate" integer not null, + "status_count" integer not null, + "is_valid" text not null, + constraint "cm_user_map_uq" unique ("profile_id", "map_id") +); + +create table "cm_user_music" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "music_id" integer not null, + "level" integer not null, + "play_count" integer not null, + "score_max" integer not null, + "res_request_count" integer not null, + "res_accept_count" integer not null, + "res_success_count" integer not null, + "miss_count" integer not null, + "max_combo_count" integer not null, + "is_full_combo" text not null, + "is_all_justice" text not null, + "is_success" text not null, + "full_chain" integer not null, + "max_chain" integer not null, + "score_rank" integer not null, + "is_lock" text not null, + constraint "cm_user_music_uq" unique ("profile_id", "music_id", "level") +); + +create table "cm_user_playlog" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "order_id" integer not null, + "sort_number" integer not null, + "place_id" integer not null, + "play_date" text not null, + "user_play_date" text not null, + "music_id" integer not null, + "level" integer not null, + "custom_id" integer not null, + "played_user_id1" integer not null, + "played_user_id2" integer not null, + "played_user_id3" integer not null, + "played_user_name1" text not null, + "played_user_name2" text not null, + "played_user_name3" text not null, + "played_music_level1" integer not null, + "played_music_level2" integer not null, + "played_music_level3" integer not null, + "played_custom1" integer not null, + "played_custom2" integer not null, + "played_custom3" integer not null, + "track" integer not null, + "score" integer not null, + "rank" integer not null, + "max_combo" integer not null, + "max_chain" integer not null, + "rate_tap" integer not null, + "rate_hold" integer not null, + "rate_slide" integer not null, + "rate_air" integer not null, + "rate_flick" integer not null, + "judge_guilty" integer not null, + "judge_attack" integer not null, + "judge_justice" integer not null, + "judge_critical" integer not null, + "event_id" integer not null, + "player_rating" integer not null, + "is_new_record" text not null, + "is_full_combo" text not null, + "full_chain_kind" integer not null, + "is_all_justice" text not null, + "is_continue" text not null, + "is_free_to_play" text not null, + "character_id" integer not null, + "skill_id" integer not null, + "play_kind" integer not null, + "is_clear" text not null, + "skill_level" integer not null, + "skill_effect" integer not null, + "place_name" text not null, + "is_maimai" text not null +); diff --git a/schema/migrate/M0008-cm-initial-deploy.sql b/schema/migrate/M0008-cm-initial-deploy.sql new file mode 100644 index 0000000..715d0d9 --- /dev/null +++ b/schema/migrate/M0008-cm-initial-deploy.sql @@ -0,0 +1,304 @@ +-- SQLite's ALTER TABLE statement is almost useless, and who knows how much of +-- this stuff is present in older versions. So while ideally we'd mark a lot +-- of these columns NOT NULL that might come back to bite us as we extend +-- support downward from Chunithm 4 to Chunithm 1. + +create table "cm_user_data" ( + "id" integer primary key not null, + -- Could in theory do a shared primary key with aime_player, since this + -- is going to be a 1:1 relation for the foreseeable future, but again + -- the SQLite strait-jacket forces us to design defensively. + "player_id" integer not null + references "aime_player"("id") + on delete cascade, + "access_code" text, + "user_name" text, + "is_web_join" text, + "web_limit_date" text, + "level" integer, + "reincarnation_num" integer, + "exp" integer, + "point" integer, + "total_point" integer, + "play_count" integer, + "multi_play_count" integer, + "multi_win_count" integer, + "request_res_count" integer, + "accept_res_count" integer, + "success_res_count" integer, + "player_rating" integer, + "highest_rating" integer, + "nameplate_id" integer, + "frame_id" integer, + "character_id" integer, + "trophy_id" integer, + "played_tutorial_bit" integer, + "first_tutorial_cancel_num" integer, + "master_tutorial_cancel_num" integer, + "total_repertoire_count" integer, + "total_map_num" integer, + "total_hi_score" integer, + "total_basic_high_score" integer, + "total_advanced_high_score" integer, + "total_expert_high_score" integer, + "total_master_high_score" integer, + "event_watched_date" text, + "friend_count" integer, + "is_maimai" text, + "first_game_id" text, + "first_rom_version" text, + "first_data_version" text, + "first_play_date" text, + "last_game_id" text, + "last_rom_version" text, + "last_data_version" text, + "last_play_date" text, + "last_place_id" integer, + "last_place_name" text, + "last_region_id" text, + "last_region_name" text, + "last_all_net_id" text, + "last_client_id" text +); + +create table "cm_user_activity" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "kind" integer not null, + -- This is just called "id" on the wire but, well, we already have an "id" + -- column up there ^ + "activity_id" integer not null, + "sort_number" integer not null, + "param1" integer not null, + "param2" integer not null, + "param3" integer not null, + "param4" integer not null, + constraint "cm_user_activity_uq" unique ( + "profile_id", + "kind", + "activity_id" + ) +); + +create table "cm_user_data_ex" ( + "id" integer primary key not null + references "cm_user_data"("id") + on delete cascade, + "compatible_cm_version" text not null, + "medal" integer not null, + "map_icon_id" integer not null, + "voice_id" integer not null, + "ext1" integer not null, + "ext2" integer not null, + "ext3" integer not null, + "ext4" integer not null, + "ext5" integer not null, + "ext6" integer not null, + "ext7" integer not null, + "ext8" integer not null, + "ext9" integer not null, + "ext10" integer not null, + "ext11" integer not null, + "ext12" integer not null, + "ext13" integer not null, + "ext14" integer not null, + "ext15" integer not null, + "ext16" integer not null, + "ext17" integer not null, + "ext18" integer not null, + "ext19" integer not null, + "ext20" integer not null, + "ext_str1" text not null, + "ext_str2" text not null, + "ext_str3" text not null, + "ext_str4" text not null, + "ext_str5" text not null, + "ext_long1" integer not null, + "ext_long2" integer not null, + "ext_long3" integer not null, + "ext_long4" integer not null, + "ext_long5" integer not null +); + +create table "cm_user_character" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "character_id" integer not null, + "play_count" integer not null, + "level" integer not null, + "skill_id" integer not null, + "friendship_exp" integer not null, + "is_valid" integer not null, + "is_new_mark" integer not null, + "param1" integer not null, + "param2" integer not null, + constraint "cm_user_character_uq" unique ("profile_id", "character_id") +); + +create table "cm_user_game_option" ( + "id" integer primary key not null + references "cm_user_data"("id") + on delete cascade, + "bg_info" integer not null, + "field_color" integer not null, + "guide_sound" integer not null, + "sound_effect" integer not null, + "guide_line" integer not null, + "speed" integer not null, + "option_set" integer not null, + "matching" integer not null, + "judge_pos" integer not null, + "rating" integer not null, + "judge_justice" integer not null, + "judge_attack" integer not null, + "headphone" integer not null, + "player_level" integer not null, + "success_tap" integer not null, + "success_ex_tap" integer not null, + "success_slide_hold" integer not null, + "success_air" integer not null, + "success_flick" integer not null, + "success_skill" integer not null, + "success_tap_timbre" integer not null, + "privacy" integer not null +); + +create table "cm_user_game_option_ex" ( + "id" integer primary key not null + references "cm_user_data"("id") + on delete cascade, + "ext1" integer not null, + "ext2" integer not null, + "ext3" integer not null, + "ext4" integer not null, + "ext5" integer not null, + "ext6" integer not null, + "ext7" integer not null, + "ext8" integer not null, + "ext9" integer not null, + "ext10" integer not null, + "ext11" integer not null, + "ext12" integer not null, + "ext13" integer not null, + "ext14" integer not null, + "ext15" integer not null, + "ext16" integer not null, + "ext17" integer not null, + "ext18" integer not null, + "ext19" integer not null, + "ext20" integer not null +); + +create table "cm_user_item" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "item_kind" integer not null, + "item_id" integer not null, + "stock" integer not null, + "is_valid" text not null, + constraint "cm_user_item_uq" unique ("profile_id", "item_kind", "item_id") +); + +create table "cm_user_map" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "map_id" integer not null, + "position" integer not null, + "is_clear" text not null, + "area_id" integer not null, + "route_number" integer not null, + "event_id" integer not null, + "rate" integer not null, + "status_count" integer not null, + "is_valid" text not null, + constraint "cm_user_map_uq" unique ("profile_id", "map_id") +); + +create table "cm_user_music" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "music_id" integer not null, + "level" integer not null, + "play_count" integer not null, + "score_max" integer not null, + "res_request_count" integer not null, + "res_accept_count" integer not null, + "res_success_count" integer not null, + "miss_count" integer not null, + "max_combo_count" integer not null, + "is_full_combo" text not null, + "is_all_justice" text not null, + "is_success" text not null, + "full_chain" integer not null, + "max_chain" integer not null, + "score_rank" integer not null, + "is_lock" text not null, + constraint "cm_user_music_uq" unique ("profile_id", "music_id", "level") +); + +create table "cm_user_playlog" ( + "id" integer primary key not null, + "profile_id" integer not null + references "cm_user_data"("id") + on delete cascade, + "order_id" integer not null, + "sort_number" integer not null, + "place_id" integer not null, + "play_date" text not null, + "user_play_date" text not null, + "music_id" integer not null, + "level" integer not null, + "custom_id" integer not null, + "played_user_id1" integer not null, + "played_user_id2" integer not null, + "played_user_id3" integer not null, + "played_user_name1" text not null, + "played_user_name2" text not null, + "played_user_name3" text not null, + "played_music_level1" integer not null, + "played_music_level2" integer not null, + "played_music_level3" integer not null, + "played_custom1" integer not null, + "played_custom2" integer not null, + "played_custom3" integer not null, + "track" integer not null, + "score" integer not null, + "rank" integer not null, + "max_combo" integer not null, + "max_chain" integer not null, + "rate_tap" integer not null, + "rate_hold" integer not null, + "rate_slide" integer not null, + "rate_air" integer not null, + "rate_flick" integer not null, + "judge_guilty" integer not null, + "judge_attack" integer not null, + "judge_justice" integer not null, + "judge_critical" integer not null, + "event_id" integer not null, + "player_rating" integer not null, + "is_new_record" text not null, + "is_full_combo" text not null, + "full_chain_kind" integer not null, + "is_all_justice" text not null, + "is_continue" text not null, + "is_free_to_play" text not null, + "character_id" integer not null, + "skill_id" integer not null, + "play_kind" integer not null, + "is_clear" text not null, + "skill_level" integer not null, + "skill_effect" integer not null, + "place_name" text not null, + "is_maimai" text not null +); diff --git a/src/checkdb.ts b/src/checkdb.ts index d772748..8389821 100644 --- a/src/checkdb.ts +++ b/src/checkdb.ts @@ -9,6 +9,7 @@ const scripts = [ // Order matters! Later scripts reference schema instantiated earlier. "meta.sql", "aime.sql", + "cm.sql", "idz.sql", ]; diff --git a/src/chunithm/getGameCharge.ts b/src/chunithm/getGameCharge.ts deleted file mode 100644 index cdccf63..0000000 --- a/src/chunithm/getGameCharge.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameCharge(req: Request, res: Response) { - res.json({ - length: 0, - gameChargeList: [ - /* - { - orderId: 1, - chargeId: 1, - price: 1, - startDate: 'STRINGIDK', - endDate: 'STRINGIDK', - salePrice: - saleStartDate: 'STRINGIDK', - saleEndDate: 'STRINGIDK', - }, - */ - ], - }); -} diff --git a/src/chunithm/getGameEvent.ts b/src/chunithm/getGameEvent.ts deleted file mode 100644 index e58cb8b..0000000 --- a/src/chunithm/getGameEvent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameEvent(req: Request, res: Response) { - res.json({ - type: 1, - length: 0, - gameEventList: [ - /* - { - type: 1, - id: 1102, // data/A000/event/event00001102 - startDate: 'STRINGIDK', - endDate: 'STRINGIDK', - }, - */ - ], - }); -} diff --git a/src/chunithm/getGameIdlist.ts b/src/chunithm/getGameIdlist.ts deleted file mode 100644 index 2d689cf..0000000 --- a/src/chunithm/getGameIdlist.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameIdlist(req: Request, res: Response) { - const { type } = req.body; - - res.json({ - type, - length: 0, - gameIdlistList: [], - }); -} diff --git a/src/chunithm/getGameMessage.ts b/src/chunithm/getGameMessage.ts deleted file mode 100644 index 9fe5ca6..0000000 --- a/src/chunithm/getGameMessage.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameMessage(req: Request, res: Response) { - res.json({ - type: 1, - length: 0, - gameMessageList: [ - /* { - type: 2, - id: 1, - message: "true", - startDate: "0", - endDate: "0" - },*/ - ], - }); -} diff --git a/src/chunithm/getGameRanking.ts b/src/chunithm/getGameRanking.ts deleted file mode 100644 index 53c00d0..0000000 --- a/src/chunithm/getGameRanking.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameRanking(req: Request, res: Response) { - const { type } = req.body; - - res.json({ - type, - gameRankingList: [ - /* - // QWORD fields maybe? - { - id: 1, - point: 1, - } - */ - ], - }); -} diff --git a/src/chunithm/getGameSale.ts b/src/chunithm/getGameSale.ts deleted file mode 100644 index 72b533c..0000000 --- a/src/chunithm/getGameSale.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameSale(req: Request, res: Response) { - const { type } = req.body; - - res.json({ - type, - length: 0, - gameSaleList: [ - /* - { - orderId: 1234, - type, - id: 4321, - rate: 5678, - startDate: 'STRINGIDK', - endDate: 'STRINGIDK', - }, - */ - ], - }); -} diff --git a/src/chunithm/getGameSetting.ts b/src/chunithm/getGameSetting.ts deleted file mode 100644 index 33377ae..0000000 --- a/src/chunithm/getGameSetting.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameSetting(req: Request, res: Response) { - res.json({ - gameSetting: { - dataVersion: 1, - isMaintenance: false, - requestInterval: 10, - rebootStartTime: 0, - rebootEndTime: 0, - isBackgroundDistribute: false, - maxCountCharacter: 999, - maxCountItem: 999, - maxCountMusic: 999, - }, - isDumpUpload: false, - isAou: false, - }); -} diff --git a/src/chunithm/handler/_util.ts b/src/chunithm/handler/_util.ts new file mode 100644 index 0000000..d5ed248 --- /dev/null +++ b/src/chunithm/handler/_util.ts @@ -0,0 +1,13 @@ +// Make the names explicit so that we don't get the order mixed up +interface Params { + maxCount: number; + nextIndex: number; +} + +export function paginationCookie(items: T[], params: Params) { + if (items.length < params.maxCount) { + return "-1"; + } else { + return (params.nextIndex + params.maxCount).toString(); + } +} diff --git a/src/chunithm/handler/gameLogin.ts b/src/chunithm/handler/gameLogin.ts new file mode 100644 index 0000000..efc917c --- /dev/null +++ b/src/chunithm/handler/gameLogin.ts @@ -0,0 +1,10 @@ +import { Repositories } from "../repo"; +import { GameLoginRequest } from "../request/gameLogin"; +import { GameLoginResponse } from "../response/gameLogin"; + +export default async function gameLogin( + rep: Repositories, + req: GameLoginRequest +): Promise { + return { returnCode: "1" }; +} diff --git a/src/chunithm/handler/gameLogout.ts b/src/chunithm/handler/gameLogout.ts new file mode 100644 index 0000000..48baa79 --- /dev/null +++ b/src/chunithm/handler/gameLogout.ts @@ -0,0 +1,10 @@ +import { Repositories } from "../repo"; +import { GameLogoutRequest } from "../request/gameLogout"; +import { GameLogoutResponse } from "../response/gameLogout"; + +export default async function gameLogout( + rep: Repositories, + req: GameLogoutRequest +): Promise { + return { returnCode: "1" }; +} diff --git a/src/chunithm/handler/getGameCharge.ts b/src/chunithm/handler/getGameCharge.ts new file mode 100644 index 0000000..5b182c1 --- /dev/null +++ b/src/chunithm/handler/getGameCharge.ts @@ -0,0 +1,13 @@ +import { Repositories } from "../repo"; +import { GetGameChargeRequest } from "../request/getGameCharge"; +import { GetGameChargeResponse } from "../response/getGameCharge"; + +export default async function getGameCharge( + rep: Repositories, + req: GetGameChargeRequest +): Promise { + return { + length: "0", + gameChargeList: [], + }; +} diff --git a/src/chunithm/handler/getGameEvent.ts b/src/chunithm/handler/getGameEvent.ts new file mode 100644 index 0000000..9d6c382 --- /dev/null +++ b/src/chunithm/handler/getGameEvent.ts @@ -0,0 +1,14 @@ +import { Repositories } from "../repo"; +import { GetGameEventRequest } from "../request/getGameEvent"; +import { GetGameEventResponse } from "../response/getGameEvent"; + +export default async function getGameEvent( + rep: Repositories, + req: GetGameEventRequest +): Promise { + return { + type: req.type, + length: "0", + gameEventList: [], + }; +} diff --git a/src/chunithm/handler/getGameIdlist.ts b/src/chunithm/handler/getGameIdlist.ts new file mode 100644 index 0000000..5d086b8 --- /dev/null +++ b/src/chunithm/handler/getGameIdlist.ts @@ -0,0 +1,14 @@ +import { Repositories } from "../repo"; +import { GetGameIdlistRequest } from "../request/getGameIdlist"; +import { GetGameIdlistResponse } from "../response/getGameIdlist"; + +export default async function getGameIdlist( + rep: Repositories, + req: GetGameIdlistRequest +): Promise { + return { + type: req.type, + length: "0", + gameIdlistList: [], + }; +} diff --git a/src/chunithm/handler/getGameMessage.ts b/src/chunithm/handler/getGameMessage.ts new file mode 100644 index 0000000..bf9642f --- /dev/null +++ b/src/chunithm/handler/getGameMessage.ts @@ -0,0 +1,14 @@ +import { Repositories } from "../repo"; +import { GetGameMessageRequest } from "../request/getGameMessage"; +import { GetGameMessageResponse } from "../response/getGameMessage"; + +export default async function getGameMessage( + rep: Repositories, + req: GetGameMessageRequest +): Promise { + return { + type: req.type, + length: "0", + gameMessageList: [], + }; +} diff --git a/src/chunithm/handler/getGameRanking.ts b/src/chunithm/handler/getGameRanking.ts new file mode 100644 index 0000000..f4d7615 --- /dev/null +++ b/src/chunithm/handler/getGameRanking.ts @@ -0,0 +1,13 @@ +import { Repositories } from "../repo"; +import { GetGameRankingRequest } from "../request/getGameRanking"; +import { GetGameRankingResponse } from "../response/getGameRanking"; + +export default async function getGameRanking( + rep: Repositories, + req: GetGameRankingRequest +): Promise { + return { + type: req.type, + gameRankingList: [], + }; +} diff --git a/src/chunithm/handler/getGameSale.ts b/src/chunithm/handler/getGameSale.ts new file mode 100644 index 0000000..4341287 --- /dev/null +++ b/src/chunithm/handler/getGameSale.ts @@ -0,0 +1,14 @@ +import { Repositories } from "../repo"; +import { GetGameSaleRequest } from "../request/getGameSale"; +import { GetGameSaleResponse } from "../response/getGameSale"; + +export default async function getGameSale( + rep: Repositories, + req: GetGameSaleRequest +): Promise { + return { + type: req.type, + length: "0", + gameSaleList: [], + }; +} diff --git a/src/chunithm/handler/getGameSetting.ts b/src/chunithm/handler/getGameSetting.ts new file mode 100644 index 0000000..e096afa --- /dev/null +++ b/src/chunithm/handler/getGameSetting.ts @@ -0,0 +1,24 @@ +import { Repositories } from "../repo"; +import { GetGameSettingRequest } from "../request/getGameSetting"; +import { GetGameSettingResponse } from "../response/getGameSetting"; + +export default async function getGameSetting( + rep: Repositories, + req: GetGameSettingRequest +): Promise { + return { + gameSetting: { + dataVersion: "1", + isMaintenance: "false", + requestInterval: "10", + rebootStartTime: "0", + rebootEndTime: "0", + isBackgroundDistribute: "false", + maxCountCharacter: "999", + maxCountItem: "999", + maxCountMusic: "999", + }, + isDumpUpload: "false", + isAou: "false", + }; +} diff --git a/src/chunithm/handler/getUserActivity.ts b/src/chunithm/handler/getUserActivity.ts new file mode 100644 index 0000000..cf0afb7 --- /dev/null +++ b/src/chunithm/handler/getUserActivity.ts @@ -0,0 +1,23 @@ +import { readAimeId } from "../proto/base"; +import { writeUserActivity } from "../proto/userActivity"; +import { Repositories } from "../repo"; +import { GetUserActivityRequest } from "../request/getUserActivity"; +import { GetUserActivityResponse } from "../response/getUserActivity"; + +export default async function getUserActivity( + rep: Repositories, + req: GetUserActivityRequest +): Promise { + const aimeId = readAimeId(req.userId); + const kind = parseInt(req.kind); + + const profileId = await rep.userData().lookup(aimeId); + const items = await rep.userActivity().load(profileId, kind); + + return { + userId: req.userId, + length: items.length.toString(), + kind: kind.toString(), + userActivityList: items.map(writeUserActivity), + }; +} diff --git a/src/chunithm/handler/getUserCharacter.ts b/src/chunithm/handler/getUserCharacter.ts new file mode 100644 index 0000000..0c84cbb --- /dev/null +++ b/src/chunithm/handler/getUserCharacter.ts @@ -0,0 +1,27 @@ +import { paginationCookie } from "./_util"; +import { readAimeId } from "../proto/base"; +import { writeUserCharacter } from "../proto/userCharacter"; +import { Repositories } from "../repo"; +import { GetUserCharacterRequest } from "../request/getUserCharacter"; +import { GetUserCharacterResponse } from "../response/getUserCharacter"; + +export default async function getUserCharacter( + rep: Repositories, + req: GetUserCharacterRequest +): Promise { + const aimeId = readAimeId(req.userId); + const maxCount = parseInt(req.maxCount); + const nextIndex = parseInt(req.nextIndex); + + const profileId = await rep.userData().lookup(aimeId); + const items = await rep + .userCharacter() + .load(profileId, { limit: maxCount, offset: nextIndex }); + + return { + userId: req.userId, + length: items.length.toString(), + nextIndex: paginationCookie(items, { maxCount, nextIndex }), + userCharacterList: items.map(writeUserCharacter), + }; +} diff --git a/src/chunithm/handler/getUserCharge.ts b/src/chunithm/handler/getUserCharge.ts new file mode 100644 index 0000000..f1e68ff --- /dev/null +++ b/src/chunithm/handler/getUserCharge.ts @@ -0,0 +1,14 @@ +import { Repositories } from "../repo"; +import { GetUserChargeRequest } from "../request/getUserCharge"; +import { GetUserChargeResponse } from "../response/getUserCharge"; + +export default async function getUserCharge( + rep: Repositories, + req: GetUserChargeRequest +): Promise { + return { + userId: req.userId, + length: "0", + userChargeList: [], + }; +} diff --git a/src/chunithm/handler/getUserCourse.ts b/src/chunithm/handler/getUserCourse.ts new file mode 100644 index 0000000..8f6e210 --- /dev/null +++ b/src/chunithm/handler/getUserCourse.ts @@ -0,0 +1,17 @@ +import { Repositories } from "../repo"; +import { GetUserCourseRequest } from "../request/getUserCourse"; +import { GetUserCourseResponse } from "../response/getUserCourse"; + +export default async function getUserCourse( + rep: Repositories, + req: GetUserCourseRequest +): Promise { + return { + userId: req.userId, + length: "0", + nextIndex: "-1", + userCourseList: [ + // This will get saved at some point I think + ], + }; +} diff --git a/src/chunithm/handler/getUserData.ts b/src/chunithm/handler/getUserData.ts new file mode 100644 index 0000000..260e537 --- /dev/null +++ b/src/chunithm/handler/getUserData.ts @@ -0,0 +1,20 @@ +import { readAimeId } from "../proto/base"; +import { writeUserData } from "../proto/userData"; +import { Repositories } from "../repo"; +import { GetUserDataRequest } from "../request/getUserData"; +import { GetUserDataResponse } from "../response/getUserData"; + +export default async function getUserData( + rep: Repositories, + req: GetUserDataRequest +): Promise { + const aimeId = readAimeId(req.userId); + + const profileId = await rep.userData().lookup(aimeId); + const userData = await rep.userData().load(profileId); + + return { + userId: req.userId, + userData: writeUserData(userData), + }; +} diff --git a/src/chunithm/handler/getUserDataEx.ts b/src/chunithm/handler/getUserDataEx.ts new file mode 100644 index 0000000..4c29ecb --- /dev/null +++ b/src/chunithm/handler/getUserDataEx.ts @@ -0,0 +1,20 @@ +import { readAimeId } from "../proto/base"; +import { writeUserDataEx } from "../proto/userDataEx"; +import { Repositories } from "../repo"; +import { GetUserDataExRequest } from "../request/getUserDataEx"; +import { GetUserDataExResponse } from "../response/getUserDataEx"; + +export default async function getUserDataEx( + rep: Repositories, + req: GetUserDataExRequest +): Promise { + const aimeId = readAimeId(req.userId); + + const profileId = await rep.userData().lookup(aimeId); + const userDataEx = await rep.userDataEx().load(profileId); + + return { + userId: req.userId, + userDataEx: writeUserDataEx(userDataEx), + }; +} diff --git a/src/chunithm/handler/getUserDuel.ts b/src/chunithm/handler/getUserDuel.ts new file mode 100644 index 0000000..8a05453 --- /dev/null +++ b/src/chunithm/handler/getUserDuel.ts @@ -0,0 +1,14 @@ +import { Repositories } from "../repo"; +import { GetUserDuelRequest } from "../request/getUserDuel"; +import { GetUserDuelResponse } from "../response/getUserDuel"; + +export default async function getUserDuel( + rep: Repositories, + req: GetUserDuelRequest +): Promise { + return { + userId: req.userId, + length: "0", + userDuelList: [], + }; +} diff --git a/src/chunithm/handler/getUserItem.ts b/src/chunithm/handler/getUserItem.ts new file mode 100644 index 0000000..5280947 --- /dev/null +++ b/src/chunithm/handler/getUserItem.ts @@ -0,0 +1,62 @@ +import { readAimeId } from "../proto/base"; +import { writeUserItem } from "../proto/userItem"; +import { Repositories } from "../repo"; +import { GetUserItemRequest } from "../request/getUserItem"; +import { GetUserItemResponse } from "../response/getUserItem"; +import { UserItemItem } from "../model/userItem"; + +// I'm not sure why they went to all this trouble just to avoid passing an +// explicit `itemKind` parameter in the request... + +const itemKindMul = 10000000000n; + +export default async function getUserItem( + rep: Repositories, + req: GetUserItemRequest +): Promise { + // Split the "nextIndex" parameter + + const compoundIn = BigInt(req.nextIndex); + const itemKindN = compoundIn / itemKindMul; + const nextIndexN = compoundIn % itemKindMul; + + // Now unpack the entire request + + const aimeId = readAimeId(req.userId); + const maxCount = parseInt(req.maxCount); + const itemKind = parseInt(itemKindN as any); + const nextIndex = parseInt(nextIndexN as any); + + // Hit DB + // This gets called for not-yet-created profiles for some reason (probably + // so that the server can force some bonus items into a new profile's + // inventory)? So we need to gracefully handle that condition. + + // (maybe some sort of anchor row is created on first GameLogin...) + + const profileId = await rep.userData().tryLookup(aimeId); + let items: UserItemItem[]; + + if (profileId !== undefined) { + items = await rep + .userItem() + .load(profileId, itemKind, { limit: maxCount, offset: nextIndex }); + } else { + items = []; + } + + // Pack the next pagination cookie into bigint compound form + + const compoundOut = + itemKindN * itemKindMul + BigInt(maxCount + items.length); + + // Done: + + return { + userId: req.userId, + length: items.length.toString(), + nextIndex: items.length < maxCount ? "-1" : compoundOut.toString(), + itemKind: itemKind.toString(), + userItemList: items.map(writeUserItem), + }; +} diff --git a/src/chunithm/handler/getUserMap.ts b/src/chunithm/handler/getUserMap.ts new file mode 100644 index 0000000..7dfe59b --- /dev/null +++ b/src/chunithm/handler/getUserMap.ts @@ -0,0 +1,21 @@ +import { readAimeId } from "../proto/base"; +import { writeUserMap } from "../proto/userMap"; +import { Repositories } from "../repo"; +import { GetUserMapRequest } from "../request/getUserMap"; +import { GetUserMapResponse } from "../response/getUserMap"; + +export default async function getUserMap( + rep: Repositories, + req: GetUserMapRequest +): Promise { + const aimeId = readAimeId(req.userId); + + const profileId = await rep.userData().lookup(aimeId); + const items = await rep.userMap().load(profileId); + + return { + userId: req.userId, + length: items.length.toString(), + userMapList: items.map(writeUserMap), + }; +} diff --git a/src/chunithm/handler/getUserMusic.ts b/src/chunithm/handler/getUserMusic.ts new file mode 100644 index 0000000..fe3316c --- /dev/null +++ b/src/chunithm/handler/getUserMusic.ts @@ -0,0 +1,50 @@ +import { writeUserMusicDetail, UserMusicJson } from "../proto/userMusic"; +import { Repositories } from "../repo"; +import { GetUserMusicRequest } from "../request/getUserMusic"; +import { GetUserMusicResponse } from "../response/getUserMusic"; +import { readAimeId } from "../proto/base"; +import { UserMusicDetailItem } from "../model/userMusic"; +import { paginationCookie } from "./_util"; + +export default async function getUserMusic( + rep: Repositories, + req: GetUserMusicRequest +): Promise { + const aimeId = readAimeId(req.userId); + const maxCount = parseInt(req.maxCount); + const nextIndex = parseInt(req.nextIndex); + + const profileId = await rep.userData().lookup(aimeId); + const flat = await rep + .userMusic() + .load(profileId, { limit: maxCount, offset: nextIndex }); + + const groupIdx = new Map(); + + for (const item of flat) { + const { musicId } = item; + const existing = groupIdx.get(musicId); + + if (existing !== undefined) { + existing.push(item); + } else { + groupIdx.set(musicId, [item]); + } + } + + const userMusicList = new Array(); + + for (const group of groupIdx.values()) { + userMusicList.push({ + length: group.length.toString(), + userMusicDetailList: group.map(writeUserMusicDetail), + }); + } + + return { + userId: req.userId, + length: userMusicList.length.toString(), + nextIndex: paginationCookie(flat, { maxCount, nextIndex }), + userMusicList, + }; +} diff --git a/src/chunithm/handler/getUserOption.ts b/src/chunithm/handler/getUserOption.ts new file mode 100644 index 0000000..f71ece4 --- /dev/null +++ b/src/chunithm/handler/getUserOption.ts @@ -0,0 +1,20 @@ +import { readAimeId } from "../proto/base"; +import { writeUserGameOption } from "../proto/userGameOption"; +import { Repositories } from "../repo"; +import { GetUserOptionRequest } from "../request/getUserOption"; +import { GetUserOptionResponse } from "../response/getUserOption"; + +export default async function getUserOption( + rep: Repositories, + req: GetUserOptionRequest +): Promise { + const aimeId = readAimeId(req.userId); + + const profileId = await rep.userData().lookup(aimeId); + const userGameOption = await rep.userGameOption().load(profileId); + + return { + userId: req.userId, + userGameOption: writeUserGameOption(userGameOption), + }; +} diff --git a/src/chunithm/handler/getUserOptionEx.ts b/src/chunithm/handler/getUserOptionEx.ts new file mode 100644 index 0000000..91e2352 --- /dev/null +++ b/src/chunithm/handler/getUserOptionEx.ts @@ -0,0 +1,20 @@ +import { readAimeId } from "../proto/base"; +import { writeUserGameOptionEx } from "../proto/userGameOptionEx"; +import { Repositories } from "../repo"; +import { GetUserOptionExRequest } from "../request/getUserOptionEx"; +import { GetUserOptionExResponse } from "../response/getUserOptionEx"; + +export default async function getUserOptionEx( + rep: Repositories, + req: GetUserOptionExRequest +): Promise { + const aimeId = readAimeId(req.userId); + + const profileId = await rep.userData().lookup(aimeId); + const userGameOptionEx = await rep.userGameOptionEx().load(profileId); + + return { + userId: req.userId, + userGameOptionEx: writeUserGameOptionEx(userGameOptionEx), + }; +} diff --git a/src/chunithm/handler/getUserPreview.ts b/src/chunithm/handler/getUserPreview.ts new file mode 100644 index 0000000..ff79410 --- /dev/null +++ b/src/chunithm/handler/getUserPreview.ts @@ -0,0 +1,58 @@ +import { readAimeId, writeDate } from "../proto/base"; +import { writeUserCharacter } from "../proto/userCharacter"; +import { Repositories } from "../repo"; +import { GetUserPreviewRequest } from "../request/getUserPreview"; +import { GetUserPreviewResponse } from "../response/getUserPreview"; + +export default async function getUserPreview( + rep: Repositories, + req: GetUserPreviewRequest +): Promise { + const aimeId = readAimeId(req.userId); + const profileId = await rep.userData().tryLookup(aimeId); + + // Return an empty JSON object if the player lacks a profile; this will + // start the new profile registration flow in the client. + + if (profileId === undefined) { + return null; + } + + const userData = await rep.userData().load(profileId); + const userCharacter = await rep + .userCharacter() + .loadOne(profileId, userData.characterId); + const userGameOption = await rep.userGameOption().load(profileId); + + return { + userId: "1", + + // Current login (i.e. profile lock) status + + isLogin: "false", + lastLoginDate: "1970-01-01 09:00:00", + + // UserData: + + userName: userData.userName, + reincarnationNum: userData.reincarnationNum.toString(), + level: userData.level.toString(), + exp: userData.exp.toString(), + playerRating: userData.playerRating.toString(), + lastGameId: userData.lastGameId, + lastRomVersion: userData.lastRomVersion, + lastDataVersion: userData.lastDataVersion, + lastPlayDate: writeDate(userData.lastPlayDate), + trophyId: userData.trophyId.toString(), + + // Currently selected UserCharacter: + + userCharacter: writeUserCharacter(userCharacter), + + // UserGameOption: + + playerLevel: userGameOption.playerLevel.toString(), + rating: userGameOption.rating.toString(), + headphone: userGameOption.headphone.toString(), + }; +} diff --git a/src/chunithm/handler/getUserRecentRating.ts b/src/chunithm/handler/getUserRecentRating.ts new file mode 100644 index 0000000..df6a5ab --- /dev/null +++ b/src/chunithm/handler/getUserRecentRating.ts @@ -0,0 +1,16 @@ +import { Repositories } from "../repo"; +import { GetUserRecentRatingRequest } from "../request/getUserRecentRating"; +import { GetUserRecentRatingResponse } from "../response/getUserRecentRating"; + +export default async function getUserRecentRating( + rep: Repositories, + req: GetUserRecentRatingRequest +): Promise { + // I don't really know what subset of data to return here. + + return { + userId: req.userId, + length: "0", + userRecentRatingList: [], + }; +} diff --git a/src/chunithm/handler/getUserRegion.ts b/src/chunithm/handler/getUserRegion.ts new file mode 100644 index 0000000..aa67462 --- /dev/null +++ b/src/chunithm/handler/getUserRegion.ts @@ -0,0 +1,16 @@ +import { Repositories } from "../repo"; +import { GetUserRegionRequest } from "../request/getUserRegion"; +import { GetUserRegionResponse } from "../response/getUserRegion"; + +export default async function getUserRegion( + rep: Repositories, + req: GetUserRegionRequest +): Promise { + return { + userId: req.userId, + length: "0", + userRegionList: [ + // Doesn't get saved + ], + }; +} diff --git a/src/chunithm/handler/index.ts b/src/chunithm/handler/index.ts new file mode 100644 index 0000000..9caf7b3 --- /dev/null +++ b/src/chunithm/handler/index.ts @@ -0,0 +1,126 @@ +import compression from "compression"; +import express, { NextFunction, Request, Response } from "express"; +import logger from "debug"; + +import gameLogin from "./gameLogin"; +import gameLogout from "./gameLogout"; +import getGameCharge from "./getGameCharge"; +import getGameEvent from "./getGameEvent"; +import getGameIdlist from "./getGameIdlist"; +import getGameMessage from "./getGameMessage"; +import getGameRanking from "./getGameRanking"; +import getGameSale from "./getGameSale"; +import getGameSetting from "./getGameSetting"; +import getUserActivity from "./getUserActivity"; +import getUserCharacter from "./getUserCharacter"; +import getUserCharge from "./getUserCharge"; +import getUserCourse from "./getUserCourse"; +import getUserData from "./getUserData"; +import getUserDataEx from "./getUserDataEx"; +import getUserDuel from "./getUserDuel"; +import getUserItem from "./getUserItem"; +import getUserMap from "./getUserMap"; +import getUserMusic from "./getUserMusic"; +import getUserOption from "./getUserOption"; +import getUserOptionEx from "./getUserOptionEx"; +import getUserPreview from "./getUserPreview"; +import getUserRecentRating from "./getUserRecentRating"; +import getUserRegion from "./getUserRegion"; +import upsertClientBookkeeping from "./upsertClientBookkeeping"; +import upsertClientDevelop from "./upsertClientDevelop"; +import upsertClientError from "./upsertClientError"; +import upsertClientSetting from "./upsertClientSetting"; +import upsertClientTestmode from "./upsertClientTestmode"; +import upsertUserAll from "./upsertUserAll"; +import createSqlWrapper from "../sql"; +import { DataSource } from "../../sql"; + +const debug = logger("app:chuni:io"); + +// Thankfully we can use standard middleware for JSON I/O. We have to use a +// zlib middleware as well because compression is non-optional: the client will +// attempt to inflate whatever response we send to it whether there's a +// Transfer-Encoding header or not. + +function quirks(req: Request, res: Response, next: NextFunction) { + // Chunithm client does not follow the HTTP spec: user agents must indicate + // that they are willing to accept a particular Transfer-Encoding for that + // T-E to be used (unless the T-E in question is "chunked" but w/e), and this + // indication is advisory anyway, the server is permitted to ignore it. + // + // Force this header into the request, since it is not present normally. + + // ... unless, that is, the client is actually somebody debugging with curl + // and not the real game client, in which case this forced deflate can be + // annoying. Pass an "X-Debug" request header to suppress forced compression. + + if (req.headers["x-debug"] === undefined) { + req.headers["accept-encoding"] = "deflate"; + } + + return next(); +} + +function trace(req: Request, res: Response, next: NextFunction) { + debug("\n--- Chunithm %s ---\n", req.url); + debug("Request: %j", req.body); + + const prevJson = res.json; + + res.json = function(obj) { + debug("Response: %j", obj); + + res.json = prevJson; + res.json.apply(this, arguments); + + return res; + }; + + return next(); +} + +export default function chunithm(db: DataSource) { + const wrapper = createSqlWrapper(db); + + wrapper.rpc("/GameLoginApi", gameLogin); + wrapper.rpc("/GameLogoutApi", gameLogout); + wrapper.rpc("/GetGameChargeApi", getGameCharge); + wrapper.rpc("/GetGameEventApi", getGameEvent); + wrapper.rpc("/GetGameIdlistApi", getGameIdlist); + wrapper.rpc("/GetGameMessageApi", getGameMessage); + wrapper.rpc("/GetGameRankingApi", getGameRanking); + wrapper.rpc("/GetGameSaleApi", getGameSale); + wrapper.rpc("/GetGameSettingApi", getGameSetting); + wrapper.rpc("/GetUserActivityApi", getUserActivity); + wrapper.rpc("/GetUserCharacterApi", getUserCharacter); + wrapper.rpc("/GetUserChargeApi", getUserCharge); + wrapper.rpc("/GetUserCourseAPi", getUserCourse); + wrapper.rpc("/GetUserDataApi", getUserData); + wrapper.rpc("/GetUserDataExApi", getUserDataEx); + wrapper.rpc("/GetUserDuelApi", getUserDuel); + wrapper.rpc("/GetUserItemApi", getUserItem); + wrapper.rpc("/GetUserMapApi", getUserMap); + wrapper.rpc("/GetUserMusicApi", getUserMusic); + wrapper.rpc("/GetUserOptionApi", getUserOption); + wrapper.rpc("/GetUserOptionExApi", getUserOptionEx); + wrapper.rpc("/GetUserPreviewApi", getUserPreview); + wrapper.rpc("/GetUserRecentRatingApi", getUserRecentRating); + wrapper.rpc("/GetUserRegionApi", getUserRegion); + wrapper.rpc("/UpsertClientBookkeepingApi", upsertClientBookkeeping); + wrapper.rpc("/UpsertClientDevelopApi", upsertClientDevelop); + wrapper.rpc("/UpsertClientErrorApi", upsertClientError); + wrapper.rpc("/UpsertClientSettingApi", upsertClientSetting); + wrapper.rpc("/UpsertClientTestmodeApi", upsertClientTestmode); + wrapper.rpc("/UpsertUserAllApi", upsertUserAll); + + const app = express(); + + app.use(quirks); + app.use(compression({ threshold: 0 })); + app.use(express.json()); + app.use(trace); + + app.use("/ChuniServlet", wrapper); + + return app; +} diff --git a/src/chunithm/handler/upsertClientBookkeeping.ts b/src/chunithm/handler/upsertClientBookkeeping.ts new file mode 100644 index 0000000..fa1f0d2 --- /dev/null +++ b/src/chunithm/handler/upsertClientBookkeeping.ts @@ -0,0 +1,12 @@ +import { Repositories } from "../repo"; +import { UpsertClientBookkeepingRequest } from "../request/upsertClientBookkeeping"; +import { UpsertClientBookkeepingResponse } from "../response/upsertClientBookkeeping"; + +export default async function upsertClientBookkeeping( + rep: Repositories, + req: UpsertClientBookkeepingRequest +): Promise { + return { + returnCode: "1", + }; +} diff --git a/src/chunithm/handler/upsertClientDevelop.ts b/src/chunithm/handler/upsertClientDevelop.ts new file mode 100644 index 0000000..2dd2735 --- /dev/null +++ b/src/chunithm/handler/upsertClientDevelop.ts @@ -0,0 +1,12 @@ +import { Repositories } from "../repo"; +import { UpsertClientDevelopRequest } from "../request/upsertClientDevelop"; +import { UpsertClientDevelopResponse } from "../response/upsertClientDevelop"; + +export default async function upsertClientDevelop( + rep: Repositories, + req: UpsertClientDevelopRequest +): Promise { + return { + returnCode: "1", + }; +} diff --git a/src/chunithm/handler/upsertClientError.ts b/src/chunithm/handler/upsertClientError.ts new file mode 100644 index 0000000..5610dfd --- /dev/null +++ b/src/chunithm/handler/upsertClientError.ts @@ -0,0 +1,12 @@ +import { Repositories } from "../repo"; +import { UpsertClientErrorRequest } from "../request/upsertClientError"; +import { UpsertClientErrorResponse } from "../response/upsertClientError"; + +export default async function upsertClientError( + rep: Repositories, + req: UpsertClientErrorRequest +): Promise { + return { + returnCode: "1", + }; +} diff --git a/src/chunithm/handler/upsertClientSetting.ts b/src/chunithm/handler/upsertClientSetting.ts new file mode 100644 index 0000000..2936d6a --- /dev/null +++ b/src/chunithm/handler/upsertClientSetting.ts @@ -0,0 +1,12 @@ +import { Repositories } from "../repo"; +import { UpsertClientSettingRequest } from "../request/upsertClientSetting"; +import { UpsertClientSettingResponse } from "../response/upsertClientSetting"; + +export default async function upsertClientSetting( + rep: Repositories, + req: UpsertClientSettingRequest +): Promise { + return { + returnCode: "1", + }; +} diff --git a/src/chunithm/handler/upsertClientTestmode.ts b/src/chunithm/handler/upsertClientTestmode.ts new file mode 100644 index 0000000..ef22ef8 --- /dev/null +++ b/src/chunithm/handler/upsertClientTestmode.ts @@ -0,0 +1,12 @@ +import { Repositories } from "../repo"; +import { UpsertClientTestmodeRequest } from "../request/upsertClientTestmode"; +import { UpsertClientTestmodeResponse } from "../response/upsertClientTestmode"; + +export default async function upsertClientTestmode( + rep: Repositories, + req: UpsertClientTestmodeRequest +): Promise { + return { + returnCode: "1", + }; +} diff --git a/src/chunithm/handler/upsertUserAll.ts b/src/chunithm/handler/upsertUserAll.ts new file mode 100644 index 0000000..d0836a9 --- /dev/null +++ b/src/chunithm/handler/upsertUserAll.ts @@ -0,0 +1,73 @@ +import { Repositories } from "../repo"; +import { readAimeId } from "../proto/base"; +import { readUserCharacter } from "../proto/userCharacter"; +import { readUserData } from "../proto/userData"; +import { readUserGameOption } from "../proto/userGameOption"; +import { UpsertUserAllRequest } from "../request/upsertUserAll"; +import { UpsertUserAllResponse } from "../response/upsertUserAll"; +import { readUserGameOptionEx } from "../proto/userGameOptionEx"; +import { readUserMap } from "../proto/userMap"; +import { readUserItem } from "../proto/userItem"; +import { readUserMusicDetail } from "../proto/userMusic"; +import { readUserActivity } from "../proto/userActivity"; +import { readUserDataEx } from "../proto/userDataEx"; +import { readUserPlaylog } from "../proto/userPlaylog"; + +// It shouldn't need to be said really, but seeing as this message (A) requires +// fairly lengthy processing and (B) is only sent right at the end of a credit, +// you should probably copy the JSON from the server's debug log and then +// validate this implementation using curl if you're planning to make any +// modifications to how things get saved. + +export default async function upsertUserAll( + rep: Repositories, + req: UpsertUserAllRequest +): Promise { + const aimeId = readAimeId(req.userId); + const payload = req.upsertUserAll; + const profile = readUserData(req.upsertUserAll.userData[0]); + + const profileId = await rep.userData().save(aimeId, profile); + + // Empty lists don't get sent at all. Not sure which are potentially empty + // so let's just assume the answer is "all of them". Hence the use of || [] + // everywhere. + + for (const item of payload.userGameOption || []) { + await rep.userGameOption().save(profileId, readUserGameOption(item)); + } + + for (const item of payload.userGameOptionEx || []) { + await rep.userGameOptionEx().save(profileId, readUserGameOptionEx(item)); + } + + for (const item of payload.userMapList || []) { + await rep.userMap().save(profileId, readUserMap(item)); + } + + for (const item of payload.userCharacterList || []) { + await rep.userCharacter().save(profileId, readUserCharacter(item)); + } + + for (const item of payload.userItemList || []) { + await rep.userItem().save(profileId, readUserItem(item)); + } + + for (const item of payload.userMusicDetailList || []) { + await rep.userMusic().save(profileId, readUserMusicDetail(item)); + } + + for (const item of payload.userActivityList || []) { + await rep.userActivity().save(profileId, readUserActivity(item)); + } + + for (const item of payload.userPlaylogList || []) { + await rep.userPlaylog().save(profileId, readUserPlaylog(item)); + } + + for (const item of payload.userDataEx || []) { + await rep.userDataEx().save(profileId, readUserDataEx(item)); + } + + return { returnCode: "1" }; +} diff --git a/src/chunithm/index.ts b/src/chunithm/index.ts index e233294..d3a8c08 100644 --- a/src/chunithm/index.ts +++ b/src/chunithm/index.ts @@ -1,75 +1,2 @@ -import compression from "compression"; -import express from "express"; -import logger from "debug"; - -import getGameCharge from "./getGameCharge"; -import getGameEvent from "./getGameEvent"; -import getGameIdlist from "./getGameIdlist"; -import getGameMessage from "./getGameMessage"; -import getGameRanking from "./getGameRanking"; -import getGameSale from "./getGameSale"; -import getGameSetting from "./getGameSetting"; -import upsertClientBookkeeping from "./upsertClientBookkeeping"; -import upsertClientDevelop from "./upsertClientDevelop"; -import upsertClientError from "./upsertClientError"; -import upsertClientSetting from "./upsertClientSetting"; -import upsertClientTestmode from "./upsertClientTestmode"; - -const debug = logger("app:chuni:io"); -const app = express(); - -// Thankfully we can use standard middleware for JSON I/O. We have to use a -// zlib middleware as well because compression is non-optional: the client will -// attempt to inflate whatever response we send to it whether there's a -// Transfer-Encoding header or not. - -app.use(function(req, res, next) { - // Chunithm client does not follow the HTTP spec: user agents must indicate - // that they are willing to accept a particular Transfer-Encoding for that - // T-E to be used (unless the T-E in question is "chunked" but w/e), and this - // indication is advisory anyway, the server is permitted to ignore it. - // - // Force this header into the request, since it is not present normally. - - req.headers["accept-encoding"] = "deflate"; - - return next(); -}); - -app.use(compression({ threshold: 0 })); -app.use(express.json()); - -// Trace requests and responses - -app.use(function(req, resp, next) { - debug("\n--- Chunithm %s ---\n", req.url); - debug("Request: %j", req.body); - - const prevJson = resp.json; - - resp.json = function(obj) { - debug("Response: %j", obj); - - resp.json = prevJson; - resp.json.apply(this, arguments); - - return resp; - }; - - next(); -}); - -app.post("/ChuniServlet/GetGameChargeApi", getGameCharge); -app.post("/ChuniServlet/GetGameEventApi", getGameEvent); -app.post("/ChuniServlet/GetGameIdlistApi", getGameIdlist); -app.post("/ChuniServlet/GetGameMessageApi", getGameMessage); -app.post("/ChuniServlet/GetGameRankingApi", getGameRanking); -app.post("/ChuniServlet/GetGameSaleApi", getGameSale); -app.post("/ChuniServlet/GetGameSettingApi", getGameSetting); -app.post("/ChuniServlet/UpsertClientBookkeepingApi", upsertClientBookkeeping); -app.post("/ChuniServlet/UpsertClientDevelopApi", upsertClientDevelop); -app.post("/ChuniServlet/UpsertClientErrorApi", upsertClientError); -app.post("/ChuniServlet/UpsertClientSettingApi", upsertClientSetting); -app.post("/ChuniServlet/UpsertClientTestmodeApi", upsertClientTestmode); - -export default app; +import chunithm from "./handler"; +export default chunithm; diff --git a/src/chunithm/model/gameCharge.ts b/src/chunithm/model/gameCharge.ts new file mode 100644 index 0000000..68bdaa8 --- /dev/null +++ b/src/chunithm/model/gameCharge.ts @@ -0,0 +1,10 @@ +export interface GameChargeItem { + orderId: number; + chargeId: number; + price: number; + startDate: Date; + endDate: Date; + salePrice: number; + saleStartDate: Date; + saleEndDate: Date; +} diff --git a/src/chunithm/model/gameEvent.ts b/src/chunithm/model/gameEvent.ts new file mode 100644 index 0000000..c256543 --- /dev/null +++ b/src/chunithm/model/gameEvent.ts @@ -0,0 +1,6 @@ +export interface GameEventItem { + type: number; + id: number; + startDate: Date; + endDate: Date; +} diff --git a/src/chunithm/model/gameMessage.ts b/src/chunithm/model/gameMessage.ts new file mode 100644 index 0000000..12d37f3 --- /dev/null +++ b/src/chunithm/model/gameMessage.ts @@ -0,0 +1,7 @@ +export interface GameMessageItem { + type: number; + id: number; + message: string; + startDate: Date; + endDate: Date; +} diff --git a/src/chunithm/model/gameRanking.ts b/src/chunithm/model/gameRanking.ts new file mode 100644 index 0000000..b08b795 --- /dev/null +++ b/src/chunithm/model/gameRanking.ts @@ -0,0 +1,4 @@ +export interface GameRankingItem { + id: bigint; + point: bigint; +} diff --git a/src/chunithm/model/gameSale.ts b/src/chunithm/model/gameSale.ts new file mode 100644 index 0000000..26ccfb1 --- /dev/null +++ b/src/chunithm/model/gameSale.ts @@ -0,0 +1,8 @@ +export interface GameSaleItem { + orderId: number; + type: number; + id: number; + rate: number; + startDate: Date; + endDate: Date; +} diff --git a/src/chunithm/model/userActivity.ts b/src/chunithm/model/userActivity.ts new file mode 100644 index 0000000..3bb0ea7 --- /dev/null +++ b/src/chunithm/model/userActivity.ts @@ -0,0 +1,9 @@ +export interface UserActivityItem { + kind: number; + activityId: number; // This is just called "id" in the JSON! + sortNumber: number; + param1: number; + param2: number; + param3: number; + param4: number; +} diff --git a/src/chunithm/model/userCharacter.ts b/src/chunithm/model/userCharacter.ts new file mode 100644 index 0000000..5ce7630 --- /dev/null +++ b/src/chunithm/model/userCharacter.ts @@ -0,0 +1,11 @@ +export interface UserCharacterItem { + characterId: number; + playCount: number; + level: number; + skillId: number; + friendshipExp: number; + isValid: boolean; + isNewMark: boolean; + param1: number; + param2: number; +} diff --git a/src/chunithm/model/userData.ts b/src/chunithm/model/userData.ts new file mode 100644 index 0000000..d955cad --- /dev/null +++ b/src/chunithm/model/userData.ts @@ -0,0 +1,66 @@ +export interface UserDataItem { + /** NFC LUID */ + accessCode: string; + + /** Uses wide latin chars */ + userName: string; + + isWebJoin: boolean; + webLimitDate: Date; + level: number; + reincarnationNum: number; + exp: number; + point: number; + totalPoint: bigint; + playCount: number; + multiPlayCount: number; + multiWinCount: number; + requestResCount: number; + acceptResCount: number; + successResCount: number; + playerRating: number; + highestRating: number; + nameplateId: number; + frameId: number; + characterId: number; + trophyId: number; + playedTutorialBit: number; + firstTutorialCancelNum: number; + masterTutorialCancelNum: number; + totalRepertoireCount: number; + totalMapNum: number; + totalHiScore: bigint; + totalBasicHighScore: bigint; + totalAdvancedHighScore: bigint; + totalExpertHighScore: bigint; + totalMasterHighScore: bigint; + eventWatchedDate: Date; + friendCount: number; + isMaimai: boolean; + firstGameId: string; + firstRomVersion: string; + firstDataVersion: string; + firstPlayDate: Date; + lastGameId: string; + lastRomVersion: string; + lastDataVersion: string; + lastPlayDate: Date; + + /** ALLNet place ID */ + lastPlaceId: number; + + /** ALLNet place name */ + lastPlaceName: string; + + /** ALLNet "region0" */ + lastRegionId: string; + + /** ALLNet "region_name0" */ + lastRegionName: string; + + /** ALLNet "allnet_id" string */ + lastAllNetId: string; + + /** Keychip ID */ + lastClientId: string; +} diff --git a/src/chunithm/model/userDataEx.ts b/src/chunithm/model/userDataEx.ts new file mode 100644 index 0000000..c290090 --- /dev/null +++ b/src/chunithm/model/userDataEx.ts @@ -0,0 +1,36 @@ +export interface UserDataExItem { + compatibleCmVersion: string; + medal: number; + mapIconId: number; + voiceId: number; + ext1: number; + ext2: number; + ext3: number; + ext4: number; + ext5: number; + ext6: number; + ext7: number; + ext8: number; + ext9: number; + ext10: number; + ext11: number; + ext12: number; + ext13: number; + ext14: number; + ext15: number; + ext16: number; + ext17: number; + ext18: number; + ext19: number; + ext20: number; + extStr1: string; + extStr2: string; + extStr3: string; + extStr4: string; + extStr5: string; + extLong1: bigint; + extLong2: bigint; + extLong3: bigint; + extLong4: bigint; + extLong5: bigint; +} diff --git a/src/chunithm/model/userGameOption.ts b/src/chunithm/model/userGameOption.ts new file mode 100644 index 0000000..048dcae --- /dev/null +++ b/src/chunithm/model/userGameOption.ts @@ -0,0 +1,24 @@ +export interface UserGameOptionItem { + bgInfo: number; + fieldColor: number; + guideSound: number; + soundEffect: number; + guideLine: number; + speed: number; + optionSet: number; + matching: number; + judgePos: number; + rating: number; + judgeJustice: number; + judgeAttack: number; + headphone: number; + playerLevel: number; + successTap: number; + successExTap: number; + successSlideHold: number; + successAir: number; + successFlick: number; + successSkill: number; + successTapTimbre: number; + privacy: number; +} diff --git a/src/chunithm/model/userGameOptionEx.ts b/src/chunithm/model/userGameOptionEx.ts new file mode 100644 index 0000000..7746925 --- /dev/null +++ b/src/chunithm/model/userGameOptionEx.ts @@ -0,0 +1,22 @@ +export interface UserGameOptionExItem { + ext1: number; + ext2: number; + ext3: number; + ext4: number; + ext5: number; + ext6: number; + ext7: number; + ext8: number; + ext9: number; + ext10: number; + ext11: number; + ext12: number; + ext13: number; + ext14: number; + ext15: number; + ext16: number; + ext17: number; + ext18: number; + ext19: number; + ext20: number; +} diff --git a/src/chunithm/model/userItem.ts b/src/chunithm/model/userItem.ts new file mode 100644 index 0000000..8a2409b --- /dev/null +++ b/src/chunithm/model/userItem.ts @@ -0,0 +1,6 @@ +export interface UserItemItem { + itemKind: number; + itemId: number; + stock: number; + isValid: boolean; +} diff --git a/src/chunithm/model/userMap.ts b/src/chunithm/model/userMap.ts new file mode 100644 index 0000000..10773da --- /dev/null +++ b/src/chunithm/model/userMap.ts @@ -0,0 +1,11 @@ +export interface UserMapItem { + mapId: number; + position: number; + isClear: boolean; + areaId: number; + routeNumber: number; + eventId: number; + rate: number; + statusCount: number; + isValid: boolean; +} diff --git a/src/chunithm/model/userMusic.ts b/src/chunithm/model/userMusic.ts new file mode 100644 index 0000000..597de27 --- /dev/null +++ b/src/chunithm/model/userMusic.ts @@ -0,0 +1,18 @@ +export interface UserMusicDetailItem { + musicId: number; + level: number; + playCount: number; + scoreMax: number; + resRequestCount: number; + resAcceptCount: number; + resSuccessCount: number; + missCount: number; + maxComboCount: number; + isFullCombo: boolean; + isAllJustice: boolean; + isSuccess: boolean; + fullChain: number; + maxChain: number; + scoreRank: number; + isLock: boolean; +} diff --git a/src/chunithm/model/userPlaylog.ts b/src/chunithm/model/userPlaylog.ts new file mode 100644 index 0000000..000eb69 --- /dev/null +++ b/src/chunithm/model/userPlaylog.ts @@ -0,0 +1,52 @@ +export interface UserPlaylogItem { + orderId: number; + sortNumber: number; + placeId: number; + playDate: Date; + userPlayDate: Date; + musicId: number; + level: number; + customId: number; + playedUserId1: number; + playedUserId2: number; + playedUserId3: number; + playedUserName1: string; + playedUserName2: string; + playedUserName3: string; + playedMusicLevel1: number; + playedMusicLevel2: number; + playedMusicLevel3: number; + playedCustom1: number; + playedCustom2: number; + playedCustom3: number; + track: number; + score: number; + rank: number; + maxCombo: number; + maxChain: number; + rateTap: number; + rateHold: number; + rateSlide: number; + rateAir: number; + rateFlick: number; + judgeGuilty: number; + judgeAttack: number; + judgeJustice: number; + judgeCritical: number; + eventId: number; + playerRating: number; + isNewRecord: boolean; + isFullCombo: boolean; + fullChainKind: number; + isAllJustice: boolean; + isContinue: boolean; + isFreeToPlay: boolean; + characterId: number; + skillId: number; + playKind: number; + isClear: boolean; + skillLevel: number; + skillEffect: number; + placeName: string; + isMaimai: boolean; +} diff --git a/src/chunithm/model/userRecentRating.ts b/src/chunithm/model/userRecentRating.ts new file mode 100644 index 0000000..cebd028 --- /dev/null +++ b/src/chunithm/model/userRecentRating.ts @@ -0,0 +1,6 @@ +export interface UserRecentRatingItem { + musicId: number; + difficultId: number; + romVersionCode: number; + score: number; +} diff --git a/src/chunithm/model/userRegion.ts b/src/chunithm/model/userRegion.ts new file mode 100644 index 0000000..d0aaca6 --- /dev/null +++ b/src/chunithm/model/userRegion.ts @@ -0,0 +1,5 @@ +export interface UserRegionItem { + regionId: number; + playCount: number; + created: Date; +} diff --git a/src/chunithm/proto/base.ts b/src/chunithm/proto/base.ts new file mode 100644 index 0000000..818ef29 --- /dev/null +++ b/src/chunithm/proto/base.ts @@ -0,0 +1,60 @@ +import format from "date-fns/format"; + +import { AimeId } from "../../model"; + +type ModelObject = { + [K in keyof T]: string | number | bigint | boolean | Date; +}; + +export type Crush> = { + [K in keyof T]: string; +}; + +export function readAimeId(str: string): AimeId { + return parseInt(str) as AimeId; +} + +export function readBoolean(str: string): boolean { + // The compiler will catch typos of "readBoolean". It will not catch typos + // of "true". Which sounds like a few hours of debugging I'd prefer to + // proactively avoid. + return str === "true"; +} + +export function readDate(str: string): Date { + // If we ever need to change this then we'll be able to do it from one place + return new Date(str); +} + +export function readWtf8(src: string): string { + // If you read this aloud it is pronounced "Double UTF-8". + // Which actually kind of accurately describes what's going on here. + + const buf = Buffer.alloc(src.length); + + for (let i = 0; i < src.length; i++) { + buf.writeUInt8(src.codePointAt(i)!, i); + } + + return buf.toString("utf-8"); +} + +export function writeDate(date: Date): string { + return format(date, "YYYY-MM-DD HH:mm:ss"); +} + +export function writeObject>(obj: T): Crush { + const result = {} as Crush; + + for (const k in obj) { + const v = obj[k]; + + if (v instanceof Date) { + result[k] = writeDate(v); + } else { + result[k] = v.toString(); + } + } + + return result; +} diff --git a/src/chunithm/proto/gameCharge.ts b/src/chunithm/proto/gameCharge.ts new file mode 100644 index 0000000..38ae855 --- /dev/null +++ b/src/chunithm/proto/gameCharge.ts @@ -0,0 +1,8 @@ +import { Crush, writeObject } from "./base"; +import { GameChargeItem } from "../model/gameCharge"; + +export type GameChargeJson = Crush; + +export function writeGameCharge(obj: GameChargeItem): GameChargeJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/gameEvent.ts b/src/chunithm/proto/gameEvent.ts new file mode 100644 index 0000000..f4315d0 --- /dev/null +++ b/src/chunithm/proto/gameEvent.ts @@ -0,0 +1,8 @@ +import { Crush, writeObject } from "./base"; +import { GameEventItem } from "../model/gameEvent"; + +export type GameEventJson = Crush; + +export function writeGameEvent(obj: GameEventItem): GameEventJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/gameMessage.ts b/src/chunithm/proto/gameMessage.ts new file mode 100644 index 0000000..f04d333 --- /dev/null +++ b/src/chunithm/proto/gameMessage.ts @@ -0,0 +1,8 @@ +import { Crush, writeObject } from "./base"; +import { GameMessageItem } from "../model/gameMessage"; + +export type GameMessageJson = Crush; + +export function writeGameMessage(obj: GameMessageItem): GameMessageJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/gameRanking.ts b/src/chunithm/proto/gameRanking.ts new file mode 100644 index 0000000..96a0418 --- /dev/null +++ b/src/chunithm/proto/gameRanking.ts @@ -0,0 +1,8 @@ +import { Crush, writeObject } from "./base"; +import { GameRankingItem } from "../model/gameRanking"; + +export type GameRankingJson = Crush; + +export function writeGameRanking(obj: GameRankingItem): GameRankingJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/gameSale.ts b/src/chunithm/proto/gameSale.ts new file mode 100644 index 0000000..35d1fbc --- /dev/null +++ b/src/chunithm/proto/gameSale.ts @@ -0,0 +1,8 @@ +import { Crush, writeObject } from "./base"; +import { GameSaleItem } from "../model/gameSale"; + +export type GameSaleJson = Crush; + +export function writeGameSale(obj: GameSaleItem): GameSaleJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userActivity.ts b/src/chunithm/proto/userActivity.ts new file mode 100644 index 0000000..fae0086 --- /dev/null +++ b/src/chunithm/proto/userActivity.ts @@ -0,0 +1,39 @@ +import { UserActivityItem } from "../model/userActivity"; + +export interface UserActivityJson { + // Not quite the same as our internal representation! Internally we refer to + // the "id" field as "activityId", because the SQL table that stores these + // values already has an "id" column that holds a synthetic primary key. + + kind: string; + id: string; + sortNumber: string; + param1: string; + param2: string; + param3: string; + param4: string; +} + +export function readUserActivity(json: UserActivityJson): UserActivityItem { + return { + kind: parseInt(json.kind), + activityId: parseInt(json.id), // <-- Look closely! + sortNumber: parseInt(json.sortNumber), + param1: parseInt(json.param1), + param2: parseInt(json.param2), + param3: parseInt(json.param3), + param4: parseInt(json.param4), + }; +} + +export function writeUserActivity(obj: UserActivityItem): UserActivityJson { + return { + kind: obj.kind.toString(), + id: obj.activityId.toString(), // <-- Look closely! + sortNumber: obj.sortNumber.toString(), + param1: obj.param1.toString(), + param2: obj.param2.toString(), + param3: obj.param3.toString(), + param4: obj.param4.toString(), + }; +} diff --git a/src/chunithm/proto/userCharacter.ts b/src/chunithm/proto/userCharacter.ts new file mode 100644 index 0000000..63a933f --- /dev/null +++ b/src/chunithm/proto/userCharacter.ts @@ -0,0 +1,22 @@ +import { Crush, readBoolean, writeObject } from "./base"; +import { UserCharacterItem } from "../model/userCharacter"; + +export type UserCharacterJson = Crush; + +export function readUserCharacter(json: UserCharacterJson): UserCharacterItem { + return { + characterId: parseInt(json.characterId), + playCount: parseInt(json.playCount), + level: parseInt(json.level), + skillId: parseInt(json.skillId), + friendshipExp: parseInt(json.friendshipExp), + isValid: readBoolean(json.isValid), + isNewMark: readBoolean(json.isNewMark), + param1: parseInt(json.param1), + param2: parseInt(json.param2), + }; +} + +export function writeUserCharacter(obj: UserCharacterItem): UserCharacterJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userData.ts b/src/chunithm/proto/userData.ts new file mode 100644 index 0000000..39e2bee --- /dev/null +++ b/src/chunithm/proto/userData.ts @@ -0,0 +1,68 @@ +import { Crush, readBoolean, readDate, readWtf8, writeObject } from "./base"; +import { UserDataItem } from "../model/userData"; + +export type UserDataJson = Crush; + +export function readUserData(json: UserDataJson): UserDataItem { + return { + accessCode: json.accessCode, + userName: readWtf8(json.userName), // <-- + isWebJoin: readBoolean(json.isWebJoin), + webLimitDate: readDate(json.webLimitDate), + level: parseInt(json.level), + reincarnationNum: parseInt(json.reincarnationNum), + exp: parseInt(json.exp), + point: parseInt(json.point), + totalPoint: BigInt(json.totalPoint), + playCount: parseInt(json.playCount), + multiPlayCount: parseInt(json.multiPlayCount), + multiWinCount: parseInt(json.multiWinCount), + requestResCount: parseInt(json.requestResCount), + acceptResCount: parseInt(json.acceptResCount), + successResCount: parseInt(json.successResCount), + playerRating: parseInt(json.playerRating), + highestRating: parseInt(json.highestRating), + nameplateId: parseInt(json.nameplateId), + frameId: parseInt(json.frameId), + characterId: parseInt(json.characterId), + trophyId: parseInt(json.trophyId), + playedTutorialBit: parseInt(json.playedTutorialBit), + firstTutorialCancelNum: parseInt(json.firstTutorialCancelNum), + masterTutorialCancelNum: parseInt(json.masterTutorialCancelNum), + totalRepertoireCount: parseInt(json.totalRepertoireCount), + totalMapNum: parseInt(json.totalMapNum), + totalHiScore: BigInt(json.totalHiScore), + totalBasicHighScore: BigInt(json.totalBasicHighScore), + totalAdvancedHighScore: BigInt(json.totalAdvancedHighScore), + totalExpertHighScore: BigInt(json.totalExpertHighScore), + totalMasterHighScore: BigInt(json.totalMasterHighScore), + eventWatchedDate: readDate(json.eventWatchedDate), + friendCount: parseInt(json.friendCount), + isMaimai: readBoolean(json.isMaimai), + firstGameId: json.firstGameId, + firstRomVersion: json.firstRomVersion, + firstDataVersion: json.firstDataVersion, + firstPlayDate: readDate(json.firstPlayDate), + lastGameId: json.lastGameId, + lastRomVersion: json.lastRomVersion, + lastDataVersion: json.lastDataVersion, + lastPlayDate: readDate(json.lastPlayDate), + lastPlaceId: parseInt(json.lastPlaceId), + lastPlaceName: json.lastPlaceName, + lastRegionId: json.lastRegionId, + lastRegionName: json.lastRegionName, + lastAllNetId: json.lastAllNetId, + lastClientId: json.lastClientId, + }; +} + +export function writeUserData(obj: UserDataItem): UserDataJson { + // Note: no need for a writeWtf8() for the user name. + + // Seems like something went very horribly wrong with their use of the + // boost::property_page JSON codec, but C++ and especially Boost is a hot + // mess at the best of times, so the Chunithm dev team has my sympathies + // nonetheless. + + return writeObject(obj); +} diff --git a/src/chunithm/proto/userDataEx.ts b/src/chunithm/proto/userDataEx.ts new file mode 100644 index 0000000..44c18cc --- /dev/null +++ b/src/chunithm/proto/userDataEx.ts @@ -0,0 +1,47 @@ +import { Crush, writeObject } from "./base"; +import { UserDataExItem } from "../model/userDataEx"; + +export type UserDataExJson = Crush; + +export function readUserDataEx(json: UserDataExJson): UserDataExItem { + return { + compatibleCmVersion: json.compatibleCmVersion, + medal: parseInt(json.medal), + mapIconId: parseInt(json.mapIconId), + voiceId: parseInt(json.voiceId), + ext1: parseInt(json.ext1), + ext2: parseInt(json.ext2), + ext3: parseInt(json.ext3), + ext4: parseInt(json.ext4), + ext5: parseInt(json.ext5), + ext6: parseInt(json.ext6), + ext7: parseInt(json.ext7), + ext8: parseInt(json.ext8), + ext9: parseInt(json.ext9), + ext10: parseInt(json.ext10), + ext11: parseInt(json.ext11), + ext12: parseInt(json.ext12), + ext13: parseInt(json.ext13), + ext14: parseInt(json.ext14), + ext15: parseInt(json.ext15), + ext16: parseInt(json.ext16), + ext17: parseInt(json.ext17), + ext18: parseInt(json.ext18), + ext19: parseInt(json.ext19), + ext20: parseInt(json.ext20), + extStr1: json.extStr1, + extStr2: json.extStr2, + extStr3: json.extStr3, + extStr4: json.extStr4, + extStr5: json.extStr5, + extLong1: BigInt(json.extLong1), + extLong2: BigInt(json.extLong2), + extLong3: BigInt(json.extLong3), + extLong4: BigInt(json.extLong4), + extLong5: BigInt(json.extLong5), + }; +} + +export function writeUserDataEx(obj: UserDataExItem): UserDataExJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userGameOption.ts b/src/chunithm/proto/userGameOption.ts new file mode 100644 index 0000000..82ba195 --- /dev/null +++ b/src/chunithm/proto/userGameOption.ts @@ -0,0 +1,39 @@ +import { Crush, writeObject } from "./base"; +import { UserGameOptionItem } from "../model/userGameOption"; + +export type UserGameOptionJson = Crush; + +export function readUserGameOption( + json: UserGameOptionJson +): UserGameOptionItem { + return { + bgInfo: parseInt(json.bgInfo), + fieldColor: parseInt(json.fieldColor), + guideSound: parseInt(json.guideSound), + soundEffect: parseInt(json.soundEffect), + guideLine: parseInt(json.guideLine), + speed: parseInt(json.speed), + optionSet: parseInt(json.optionSet), + matching: parseInt(json.matching), + judgePos: parseInt(json.judgePos), + rating: parseInt(json.rating), + judgeJustice: parseInt(json.judgeJustice), + judgeAttack: parseInt(json.judgeAttack), + headphone: parseInt(json.headphone), + playerLevel: parseInt(json.playerLevel), + successTap: parseInt(json.successTap), + successExTap: parseInt(json.successExTap), + successSlideHold: parseInt(json.successSlideHold), + successAir: parseInt(json.successAir), + successFlick: parseInt(json.successFlick), + successSkill: parseInt(json.successSkill), + successTapTimbre: parseInt(json.successTapTimbre), + privacy: parseInt(json.privacy), + }; +} + +export function writeUserGameOption( + obj: UserGameOptionItem +): UserGameOptionJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userGameOptionEx.ts b/src/chunithm/proto/userGameOptionEx.ts new file mode 100644 index 0000000..4259d85 --- /dev/null +++ b/src/chunithm/proto/userGameOptionEx.ts @@ -0,0 +1,37 @@ +import { Crush, writeObject } from "./base"; +import { UserGameOptionExItem } from "../model/userGameOptionEx"; + +export type UserGameOptionExJson = Crush; + +export function readUserGameOptionEx( + json: UserGameOptionExJson +): UserGameOptionExItem { + return { + ext1: parseInt(json.ext1), + ext2: parseInt(json.ext2), + ext3: parseInt(json.ext3), + ext4: parseInt(json.ext4), + ext5: parseInt(json.ext5), + ext6: parseInt(json.ext6), + ext7: parseInt(json.ext7), + ext8: parseInt(json.ext8), + ext9: parseInt(json.ext9), + ext10: parseInt(json.ext10), + ext11: parseInt(json.ext11), + ext12: parseInt(json.ext12), + ext13: parseInt(json.ext13), + ext14: parseInt(json.ext14), + ext15: parseInt(json.ext15), + ext16: parseInt(json.ext16), + ext17: parseInt(json.ext17), + ext18: parseInt(json.ext18), + ext19: parseInt(json.ext19), + ext20: parseInt(json.ext20), + }; +} + +export function writeUserGameOptionEx( + obj: UserGameOptionExItem +): UserGameOptionExJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userItem.ts b/src/chunithm/proto/userItem.ts new file mode 100644 index 0000000..1516754 --- /dev/null +++ b/src/chunithm/proto/userItem.ts @@ -0,0 +1,17 @@ +import { Crush, readBoolean, writeObject } from "./base"; +import { UserItemItem } from "../model/userItem"; + +export type UserItemJson = Crush; + +export function readUserItem(json: UserItemJson): UserItemItem { + return { + itemKind: parseInt(json.itemKind), + itemId: parseInt(json.itemId), + stock: parseInt(json.stock), + isValid: readBoolean(json.isValid), + }; +} + +export function writeUserItem(obj: UserItemItem): UserItemJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userMap.ts b/src/chunithm/proto/userMap.ts new file mode 100644 index 0000000..5b5866a --- /dev/null +++ b/src/chunithm/proto/userMap.ts @@ -0,0 +1,22 @@ +import { Crush, readBoolean, writeObject } from "./base"; +import { UserMapItem } from "../model/userMap"; + +export type UserMapJson = Crush; + +export function readUserMap(json: UserMapJson): UserMapItem { + return { + mapId: parseInt(json.mapId), + position: parseInt(json.position), + isClear: readBoolean(json.isClear), + areaId: parseInt(json.areaId), + routeNumber: parseInt(json.routeNumber), + eventId: parseInt(json.eventId), + rate: parseInt(json.rate), + statusCount: parseInt(json.statusCount), + isValid: readBoolean(json.isValid), + }; +} + +export function writeUserMap(obj: UserMapItem): UserMapJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userMusic.ts b/src/chunithm/proto/userMusic.ts new file mode 100644 index 0000000..e787ef7 --- /dev/null +++ b/src/chunithm/proto/userMusic.ts @@ -0,0 +1,49 @@ +import { Crush, readBoolean, writeObject } from "./base"; +import { UserMusicDetailItem } from "../model/userMusic"; + +/** + * Chart high score object. Saved as a flat list when the client saves new + * scores, loaded in groups when a player's high scores are loaded. + */ +export type UserMusicDetailJson = Crush; + +/** + * Grouping of `UserMusicDetailItem` objects, used when scores are loaded. + * Scores are grouped by `musicId`. + */ +export interface UserMusicJson { + /** Integer, number of sub-objects in this object */ + length: string; + + /** Scores for this music ID */ + userMusicDetailList: UserMusicDetailJson[]; +} + +export function readUserMusicDetail( + json: UserMusicDetailJson +): UserMusicDetailItem { + return { + musicId: parseInt(json.musicId), + level: parseInt(json.level), + playCount: parseInt(json.playCount), + scoreMax: parseInt(json.scoreMax), + resRequestCount: parseInt(json.resRequestCount), + resAcceptCount: parseInt(json.resAcceptCount), + resSuccessCount: parseInt(json.resSuccessCount), + missCount: parseInt(json.missCount), + maxComboCount: parseInt(json.maxComboCount), + isFullCombo: readBoolean(json.isFullCombo), + isAllJustice: readBoolean(json.isAllJustice), + isSuccess: readBoolean(json.isSuccess), + fullChain: parseInt(json.fullChain), + maxChain: parseInt(json.maxChain), + scoreRank: parseInt(json.scoreRank), + isLock: readBoolean(json.isLock), + }; +} + +export function writeUserMusicDetail( + obj: UserMusicDetailItem +): UserMusicDetailJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userPlaylog.ts b/src/chunithm/proto/userPlaylog.ts new file mode 100644 index 0000000..3341103 --- /dev/null +++ b/src/chunithm/proto/userPlaylog.ts @@ -0,0 +1,59 @@ +import { Crush, readBoolean, readDate } from "./base"; +import { UserPlaylogItem } from "../model/userPlaylog"; + +export type UserPlaylogJson = Crush; + +export function readUserPlaylog(json: UserPlaylogJson): UserPlaylogItem { + return { + orderId: parseInt(json.orderId), + sortNumber: parseInt(json.sortNumber), + placeId: parseInt(json.placeId), + playDate: readDate(json.playDate), + userPlayDate: readDate(json.userPlayDate), + musicId: parseInt(json.musicId), + level: parseInt(json.level), + customId: parseInt(json.customId), + playedUserId1: parseInt(json.playedUserId1), + playedUserId2: parseInt(json.playedUserId2), + playedUserId3: parseInt(json.playedUserId3), + playedUserName1: json.playedUserName1, + playedUserName2: json.playedUserName2, + playedUserName3: json.playedUserName3, + playedMusicLevel1: parseInt(json.playedMusicLevel1), + playedMusicLevel2: parseInt(json.playedMusicLevel2), + playedMusicLevel3: parseInt(json.playedMusicLevel3), + playedCustom1: parseInt(json.playedCustom1), + playedCustom2: parseInt(json.playedCustom2), + playedCustom3: parseInt(json.playedCustom3), + track: parseInt(json.track), + score: parseInt(json.score), + rank: parseInt(json.rank), + maxCombo: parseInt(json.maxCombo), + maxChain: parseInt(json.maxChain), + rateTap: parseInt(json.rateTap), + rateHold: parseInt(json.rateHold), + rateSlide: parseInt(json.rateSlide), + rateAir: parseInt(json.rateAir), + rateFlick: parseInt(json.rateFlick), + judgeGuilty: parseInt(json.judgeGuilty), + judgeAttack: parseInt(json.judgeAttack), + judgeJustice: parseInt(json.judgeJustice), + judgeCritical: parseInt(json.judgeCritical), + eventId: parseInt(json.eventId), + playerRating: parseInt(json.playerRating), + isNewRecord: readBoolean(json.isNewRecord), + isFullCombo: readBoolean(json.isFullCombo), + fullChainKind: parseInt(json.fullChainKind), + isAllJustice: readBoolean(json.isAllJustice), + isContinue: readBoolean(json.isContinue), + isFreeToPlay: readBoolean(json.isFreeToPlay), + characterId: parseInt(json.characterId), + skillId: parseInt(json.skillId), + playKind: parseInt(json.playKind), + isClear: readBoolean(json.isClear), + skillLevel: parseInt(json.skillLevel), + skillEffect: parseInt(json.skillEffect), + placeName: json.placeName, + isMaimai: readBoolean(json.isMaimai), + }; +} diff --git a/src/chunithm/proto/userRecentRating.ts b/src/chunithm/proto/userRecentRating.ts new file mode 100644 index 0000000..e818f1f --- /dev/null +++ b/src/chunithm/proto/userRecentRating.ts @@ -0,0 +1,21 @@ +import { Crush, writeObject } from "./base"; +import { UserRecentRatingItem } from "../model/userRecentRating"; + +export type UserRecentRatingJson = Crush; + +export function readUserRecentRating( + json: UserRecentRatingJson +): UserRecentRatingItem { + return { + musicId: parseInt(json.musicId), + difficultId: parseInt(json.difficultId), + romVersionCode: parseInt(json.romVersionCode), + score: parseInt(json.score), + }; +} + +export function writeUserRecentRating( + obj: UserRecentRatingItem +): UserRecentRatingJson { + return writeObject(obj); +} diff --git a/src/chunithm/proto/userRegion.ts b/src/chunithm/proto/userRegion.ts new file mode 100644 index 0000000..6ccfd27 --- /dev/null +++ b/src/chunithm/proto/userRegion.ts @@ -0,0 +1,16 @@ +import { Crush, readDate, writeObject } from "./base"; +import { UserRegionItem } from "../model/userRegion"; + +export type UserRegionJson = Crush; + +export function readUserRegion(json: UserRegionJson): UserRegionItem { + return { + regionId: parseInt(json.regionId), + playCount: parseInt(json.playCount), + created: readDate(json.created), + }; +} + +export function writeUserRegion(obj: UserRegionItem): UserRegionJson { + return writeObject(obj); +} diff --git a/src/chunithm/repo/_defs.ts b/src/chunithm/repo/_defs.ts new file mode 100644 index 0000000..fd14092 --- /dev/null +++ b/src/chunithm/repo/_defs.ts @@ -0,0 +1,19 @@ +import { UserDataItem } from "../model/userData"; +import { Id } from "../../model"; + +export interface Page { + offset: number; + limit: number; +} + +export interface Repository { + save(profileId: Id, obj: T): Promise; +} + +export interface Repository1 extends Repository { + load(profileId: Id): Promise; +} + +export interface RepositoryN extends Repository { + load(profileId: Id, page?: Page): Promise; +} diff --git a/src/chunithm/repo/index.ts b/src/chunithm/repo/index.ts new file mode 100644 index 0000000..6486a21 --- /dev/null +++ b/src/chunithm/repo/index.ts @@ -0,0 +1,34 @@ +export { Page } from "./_defs"; + +import { UserActivityRepository } from "./userActivity"; +import { UserCharacterRepository } from "./userCharacter"; +import { UserDataRepository } from "./userData"; +import { UserDataExRepository } from "./userDataEx"; +import { UserGameOptionRepository } from "./userGameOption"; +import { UserGameOptionExRepository } from "./userGameOptionEx"; +import { UserItemRepository } from "./userItem"; +import { UserMapRepository } from "./userMap"; +import { UserMusicRepository } from "./userMusic"; +import { UserPlaylogRepository } from "./userPlaylog"; + +export interface Repositories { + userActivity(): UserActivityRepository; + + userCharacter(): UserCharacterRepository; + + userData(): UserDataRepository; + + userDataEx(): UserDataExRepository; + + userGameOption(): UserGameOptionRepository; + + userGameOptionEx(): UserGameOptionExRepository; + + userItem(): UserItemRepository; + + userMap(): UserMapRepository; + + userMusic(): UserMusicRepository; + + userPlaylog(): UserPlaylogRepository; +} diff --git a/src/chunithm/repo/userActivity.ts b/src/chunithm/repo/userActivity.ts new file mode 100644 index 0000000..5185bdb --- /dev/null +++ b/src/chunithm/repo/userActivity.ts @@ -0,0 +1,9 @@ +import { UserActivityItem } from "../model/userActivity"; +import { UserDataItem } from "../model/userData"; +import { Id } from "../../model"; + +export interface UserActivityRepository { + load(profileId: Id, kind: number): Promise; + + save(profileId: Id, obj: UserActivityItem): Promise; +} diff --git a/src/chunithm/repo/userCharacter.ts b/src/chunithm/repo/userCharacter.ts new file mode 100644 index 0000000..05cbb81 --- /dev/null +++ b/src/chunithm/repo/userCharacter.ts @@ -0,0 +1,12 @@ +import { RepositoryN } from "./_defs"; +import { UserCharacterItem } from "../model/userCharacter"; +import { UserDataItem } from "../model/userData"; +import { Id } from "../../model"; + +export interface UserCharacterRepository + extends RepositoryN { + loadOne( + profileId: Id, + characterId: number + ): Promise; +} diff --git a/src/chunithm/repo/userData.ts b/src/chunithm/repo/userData.ts new file mode 100644 index 0000000..aa546bc --- /dev/null +++ b/src/chunithm/repo/userData.ts @@ -0,0 +1,12 @@ +import { UserDataItem } from "../model/userData"; +import { AimeId, Id } from "../../model"; + +export interface UserDataRepository { + load(profileId: Id): Promise; + + lookup(aimeId: AimeId): Promise>; + + save(aimeId: AimeId, obj: UserDataItem): Promise>; + + tryLookup(aimeId: AimeId): Promise | undefined>; +} diff --git a/src/chunithm/repo/userDataEx.ts b/src/chunithm/repo/userDataEx.ts new file mode 100644 index 0000000..fb21b9a --- /dev/null +++ b/src/chunithm/repo/userDataEx.ts @@ -0,0 +1,4 @@ +import { Repository1 } from "./_defs"; +import { UserDataExItem } from "../model/userDataEx"; + +export type UserDataExRepository = Repository1; diff --git a/src/chunithm/repo/userGameOption.ts b/src/chunithm/repo/userGameOption.ts new file mode 100644 index 0000000..d871ee5 --- /dev/null +++ b/src/chunithm/repo/userGameOption.ts @@ -0,0 +1,4 @@ +import { Repository1 } from "./_defs"; +import { UserGameOptionItem } from "../model/userGameOption"; + +export type UserGameOptionRepository = Repository1; diff --git a/src/chunithm/repo/userGameOptionEx.ts b/src/chunithm/repo/userGameOptionEx.ts new file mode 100644 index 0000000..59a2466 --- /dev/null +++ b/src/chunithm/repo/userGameOptionEx.ts @@ -0,0 +1,4 @@ +import { Repository1 } from "./_defs"; +import { UserGameOptionExItem } from "../model/userGameOptionEx"; + +export type UserGameOptionExRepository = Repository1; diff --git a/src/chunithm/repo/userItem.ts b/src/chunithm/repo/userItem.ts new file mode 100644 index 0000000..d105c54 --- /dev/null +++ b/src/chunithm/repo/userItem.ts @@ -0,0 +1,14 @@ +import { Page } from "./_defs"; +import { UserDataItem } from "../model/userData"; +import { UserItemItem } from "../model/userItem"; +import { Id } from "../../model"; + +export interface UserItemRepository { + load( + profileId: Id, + itemKind: number, + page: Page + ): Promise; + + save(profileId: Id, obj: UserItemItem): Promise; +} diff --git a/src/chunithm/repo/userMap.ts b/src/chunithm/repo/userMap.ts new file mode 100644 index 0000000..b43ee05 --- /dev/null +++ b/src/chunithm/repo/userMap.ts @@ -0,0 +1,4 @@ +import { RepositoryN } from "./_defs"; +import { UserMapItem } from "../model/userMap"; + +export type UserMapRepository = RepositoryN; diff --git a/src/chunithm/repo/userMusic.ts b/src/chunithm/repo/userMusic.ts new file mode 100644 index 0000000..51d484b --- /dev/null +++ b/src/chunithm/repo/userMusic.ts @@ -0,0 +1,4 @@ +import { RepositoryN } from "./_defs"; +import { UserMusicDetailItem } from "../model/userMusic"; + +export type UserMusicRepository = RepositoryN; diff --git a/src/chunithm/repo/userPlaylog.ts b/src/chunithm/repo/userPlaylog.ts new file mode 100644 index 0000000..60c7e48 --- /dev/null +++ b/src/chunithm/repo/userPlaylog.ts @@ -0,0 +1,10 @@ +import { UserDataItem } from "../model/userData"; +import { UserPlaylogItem } from "../model/userPlaylog"; +import { Id } from "../../model"; + +export interface UserPlaylogRepository { + // This seems to only be used internally by SEGA for analytics, but the data + // is nice to have. + + save(profileId: Id, obj: UserPlaylogItem): Promise; +} diff --git a/src/chunithm/request/gameLogin.ts b/src/chunithm/request/gameLogin.ts new file mode 100644 index 0000000..c768d7b --- /dev/null +++ b/src/chunithm/request/gameLogin.ts @@ -0,0 +1,4 @@ +export interface GameLoginRequest { + /** Integer, AiMe ID */ + userId: string; +} diff --git a/src/chunithm/request/gameLogout.ts b/src/chunithm/request/gameLogout.ts new file mode 100644 index 0000000..d1aa6a8 --- /dev/null +++ b/src/chunithm/request/gameLogout.ts @@ -0,0 +1,4 @@ +export interface GameLogoutRequest { + /** Integer, AiMe ID */ + userId: string; +} diff --git a/src/chunithm/request/getGameCharge.ts b/src/chunithm/request/getGameCharge.ts new file mode 100644 index 0000000..ed85494 --- /dev/null +++ b/src/chunithm/request/getGameCharge.ts @@ -0,0 +1,4 @@ +export interface GetGameChargeRequest { + /** Boolean */ + isAllSale: "string"; +} diff --git a/src/chunithm/request/getGameEvent.ts b/src/chunithm/request/getGameEvent.ts new file mode 100644 index 0000000..922dc45 --- /dev/null +++ b/src/chunithm/request/getGameEvent.ts @@ -0,0 +1,4 @@ +export interface GetGameEventRequest { + // FIXME + type: string; +} diff --git a/src/chunithm/request/getGameIdlist.ts b/src/chunithm/request/getGameIdlist.ts new file mode 100644 index 0000000..cd5656b --- /dev/null +++ b/src/chunithm/request/getGameIdlist.ts @@ -0,0 +1,4 @@ +export interface GetGameIdlistRequest { + /** Integer */ + type: string; +} diff --git a/src/chunithm/request/getGameMessage.ts b/src/chunithm/request/getGameMessage.ts new file mode 100644 index 0000000..c0f784a --- /dev/null +++ b/src/chunithm/request/getGameMessage.ts @@ -0,0 +1,7 @@ +export interface GetGameMessageRequest { + /** Integer */ + type: string; + + /** Boolean */ + isAllMessage: string; +} diff --git a/src/chunithm/request/getGameRanking.ts b/src/chunithm/request/getGameRanking.ts new file mode 100644 index 0000000..ef99de6 --- /dev/null +++ b/src/chunithm/request/getGameRanking.ts @@ -0,0 +1,4 @@ +export interface GetGameRankingRequest { + /** Long */ + type: string; +} diff --git a/src/chunithm/request/getGameSale.ts b/src/chunithm/request/getGameSale.ts new file mode 100644 index 0000000..dfd0a5b --- /dev/null +++ b/src/chunithm/request/getGameSale.ts @@ -0,0 +1,7 @@ +export interface GetGameSaleRequest { + /** Integer */ + type: string; + + /** Boolean */ + isAllSale: string; +} diff --git a/src/chunithm/request/getGameSetting.ts b/src/chunithm/request/getGameSetting.ts new file mode 100644 index 0000000..c7d2c3f --- /dev/null +++ b/src/chunithm/request/getGameSetting.ts @@ -0,0 +1,7 @@ +export interface GetGameSettingRequest { + /** Base-10 ALLNet location ID */ + placeId: string; + + /** Keychip ID */ + clientId: string; +} diff --git a/src/chunithm/request/getUserActivity.ts b/src/chunithm/request/getUserActivity.ts new file mode 100644 index 0000000..8678945 --- /dev/null +++ b/src/chunithm/request/getUserActivity.ts @@ -0,0 +1,7 @@ +export interface GetUserActivityRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, filter on `UserActivityItem.kind`. */ + kind: string; +} diff --git a/src/chunithm/request/getUserCharacter.ts b/src/chunithm/request/getUserCharacter.ts new file mode 100644 index 0000000..60c6695 --- /dev/null +++ b/src/chunithm/request/getUserCharacter.ts @@ -0,0 +1,10 @@ +export interface GetUserCharacterRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, pagination cookie. Initially zero. */ + nextIndex: string; + + /** Integer, max page size. Taken from GetGameSettingResponse. */ + maxCount: string; +} diff --git a/src/chunithm/request/getUserCharge.ts b/src/chunithm/request/getUserCharge.ts new file mode 100644 index 0000000..f92e343 --- /dev/null +++ b/src/chunithm/request/getUserCharge.ts @@ -0,0 +1,4 @@ +export interface GetUserChargeRequest { + /** Integer, AiMe ID */ + userId: string; +} diff --git a/src/chunithm/request/getUserCourse.ts b/src/chunithm/request/getUserCourse.ts new file mode 100644 index 0000000..1f46974 --- /dev/null +++ b/src/chunithm/request/getUserCourse.ts @@ -0,0 +1,14 @@ +export interface GetUserCourseRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, pagination cookie. Initially zero. */ + nextIndex: string; + + /** + * Integer, max page size. NOT configured from GetGameSetting as of 1.30.00, + * but instead hardcoded to 50. Still, don't assume that will always be the + * case. + */ + maxCount: string; +} diff --git a/src/chunithm/request/getUserData.ts b/src/chunithm/request/getUserData.ts new file mode 100644 index 0000000..8b5e8a7 --- /dev/null +++ b/src/chunithm/request/getUserData.ts @@ -0,0 +1,4 @@ +export interface GetUserDataRequest { + /** Integer, AiMe ID */ + userId: string; +} diff --git a/src/chunithm/request/getUserDataEx.ts b/src/chunithm/request/getUserDataEx.ts new file mode 100644 index 0000000..0b07315 --- /dev/null +++ b/src/chunithm/request/getUserDataEx.ts @@ -0,0 +1,7 @@ +export interface GetUserDataExRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Boolean */ + isAdmin: string; +} diff --git a/src/chunithm/request/getUserDuel.ts b/src/chunithm/request/getUserDuel.ts new file mode 100644 index 0000000..a2e0bac --- /dev/null +++ b/src/chunithm/request/getUserDuel.ts @@ -0,0 +1,10 @@ +export interface GetUserDuelRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Integer */ + duelId: string; + + /** Boolean */ + isAllDuel: string; +} diff --git a/src/chunithm/request/getUserItem.ts b/src/chunithm/request/getUserItem.ts new file mode 100644 index 0000000..757b3bd --- /dev/null +++ b/src/chunithm/request/getUserItem.ts @@ -0,0 +1,23 @@ +export interface GetUserItemRequest { + /** Integer, AiMe ID */ + userId: string; + + /** + * Long. + * + * This is an odd amalgam of what ought to be two separate `kind` and + * `nextIndex` parameters. Divide by 10 billion to separate them out. + * The quotient is the `kind` that the client wants you to return, the + * remainder is the effective `nextIndex` pagination cookie which comes into + * play if more than `maxCount` items of this type are available. Presumably + * returning a result set of size `maxCount` will cause another request to + * be issued in order to retrieve the next page. See response documentation + * for further details. + * + * @see GetUserItemResponse + */ + nextIndex: string; + + /** Integer, max page size. Taken from GetGameSettingResponse. */ + maxCount: string; +} diff --git a/src/chunithm/request/getUserMap.ts b/src/chunithm/request/getUserMap.ts new file mode 100644 index 0000000..d892f95 --- /dev/null +++ b/src/chunithm/request/getUserMap.ts @@ -0,0 +1,7 @@ +export interface GetUserMapRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Boolean */ + isAllMap: string; +} diff --git a/src/chunithm/request/getUserMusic.ts b/src/chunithm/request/getUserMusic.ts new file mode 100644 index 0000000..7fa0df8 --- /dev/null +++ b/src/chunithm/request/getUserMusic.ts @@ -0,0 +1,10 @@ +export interface GetUserMusicRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, pagination cookie. Initially zero. */ + nextIndex: string; + + /** Integer, max page size. Taken from GetGameSettingResponse. */ + maxCount: string; +} diff --git a/src/chunithm/request/getUserOption.ts b/src/chunithm/request/getUserOption.ts new file mode 100644 index 0000000..f60d9f8 --- /dev/null +++ b/src/chunithm/request/getUserOption.ts @@ -0,0 +1,4 @@ +export interface GetUserOptionRequest { + /** Integer, AiMe ID */ + userId: string; +} diff --git a/src/chunithm/request/getUserOptionEx.ts b/src/chunithm/request/getUserOptionEx.ts new file mode 100644 index 0000000..2b99627 --- /dev/null +++ b/src/chunithm/request/getUserOptionEx.ts @@ -0,0 +1,7 @@ +export interface GetUserOptionExRequest { + /** Integer, AiMe ID */ + userId: string; + + /** Boolean */ + isAdmin: string; +} diff --git a/src/chunithm/request/getUserPreview.ts b/src/chunithm/request/getUserPreview.ts new file mode 100644 index 0000000..4355fa5 --- /dev/null +++ b/src/chunithm/request/getUserPreview.ts @@ -0,0 +1,6 @@ +export interface GetUserPreviewRequest { + /** Integer, AiMe ID */ + userId: string; + + segaIdAuthKey: string; +} diff --git a/src/chunithm/request/getUserRecentRating.ts b/src/chunithm/request/getUserRecentRating.ts new file mode 100644 index 0000000..de9e057 --- /dev/null +++ b/src/chunithm/request/getUserRecentRating.ts @@ -0,0 +1,4 @@ +export interface GetUserRecentRatingRequest { + /** Integer, AiMe ID */ + userId: string; +} diff --git a/src/chunithm/request/getUserRegion.ts b/src/chunithm/request/getUserRegion.ts new file mode 100644 index 0000000..bfa1562 --- /dev/null +++ b/src/chunithm/request/getUserRegion.ts @@ -0,0 +1,6 @@ +export interface GetUserRegionRequest { + // FIXME + + /** Integer, AiMe ID */ + userId: string; +} diff --git a/src/chunithm/request/upsertClientBookkeeping.ts b/src/chunithm/request/upsertClientBookkeeping.ts new file mode 100644 index 0000000..ad01a67 --- /dev/null +++ b/src/chunithm/request/upsertClientBookkeeping.ts @@ -0,0 +1,3 @@ +export interface UpsertClientBookkeepingRequest { + // FIXME +} diff --git a/src/chunithm/request/upsertClientDevelop.ts b/src/chunithm/request/upsertClientDevelop.ts new file mode 100644 index 0000000..0b24aef --- /dev/null +++ b/src/chunithm/request/upsertClientDevelop.ts @@ -0,0 +1,3 @@ +export interface UpsertClientDevelopRequest { + // FIXME +} diff --git a/src/chunithm/request/upsertClientError.ts b/src/chunithm/request/upsertClientError.ts new file mode 100644 index 0000000..65c5dbb --- /dev/null +++ b/src/chunithm/request/upsertClientError.ts @@ -0,0 +1,3 @@ +export interface UpsertClientErrorRequest { + // FIXME +} diff --git a/src/chunithm/request/upsertClientSetting.ts b/src/chunithm/request/upsertClientSetting.ts new file mode 100644 index 0000000..9fcf35b --- /dev/null +++ b/src/chunithm/request/upsertClientSetting.ts @@ -0,0 +1,33 @@ +export interface UpsertClientSettingRequest { + clientSetting: { + /* Base-10 ALLNet location ID */ + placeId: string; + + /** Keychip ID */ + clientId: string; + + /** ALLNet place name */ + placeName: string; + + /** ALLNet "region0" */ + regionId: string; + + /** ALLNet "region_name0" */ + regionName: string; + + /** ALLNet "allnet_id" string */ + allNetId: string; + + /** sic. AMEX DS EEPROM ID */ + bordId: string; + + /** Data version of the form "1.30.00" */ + romVersion: string; + + /** Data version of the form "1.30.00" */ + dataVersion: string; + + /** Integer */ + dumpFileNum: string; + }; +} diff --git a/src/chunithm/request/upsertClientTestmode.ts b/src/chunithm/request/upsertClientTestmode.ts new file mode 100644 index 0000000..3c7bfc6 --- /dev/null +++ b/src/chunithm/request/upsertClientTestmode.ts @@ -0,0 +1,36 @@ +export interface UpsertClientTestmodeRequest { + clientTestmode: { + /* Base-10 ALLNet location ID */ + placeId: string; + + /** Keychip ID */ + clientId: string; + + /** Date "YYYY-MM-DD hh:mm:ss" */ + updateDate: string; + + /** Boolean */ + isDelivery: string; + + /** Integer */ + groupId: string; + + /** Integer */ + groupRole: string; + + /** Integer */ + continueMode: string; + + /** Integer */ + selectMusicTime: string; + + /** Integer */ + advertiseVolume: string; + + /** Integer */ + eventMode: string; + + /** Integer */ + eventMusicNum: string; + }; +} diff --git a/src/chunithm/request/upsertUserAll.ts b/src/chunithm/request/upsertUserAll.ts new file mode 100644 index 0000000..b7ee95b --- /dev/null +++ b/src/chunithm/request/upsertUserAll.ts @@ -0,0 +1,48 @@ +import { UserDataJson } from "../proto/userData"; +import { UserGameOptionJson } from "../proto/userGameOption"; +import { UserGameOptionExJson } from "../proto/userGameOptionEx"; +import { UserMapJson } from "../proto/userMap"; +import { UserCharacterJson } from "../proto/userCharacter"; +import { UserItemJson } from "../proto/userItem"; +import { UserMusicDetailJson } from "../proto/userMusic"; +import { UserActivityJson } from "../proto/userActivity"; +import { UserRecentRatingJson } from "../proto/userRecentRating"; +import { UserPlaylogJson } from "../proto/userPlaylog"; +import { UserDataExJson } from "../proto/userDataEx"; + +export interface UpsertUserAllRequest { + /** Integer, AiMe ID */ + userId: string; + + upsertUserAll: { + userData: UserDataJson[]; + userGameOption?: UserGameOptionJson[]; + userGameOptionEx?: UserGameOptionExJson[]; + userMapList?: UserMapJson[]; + userCharacterList?: UserCharacterJson[]; + userItemList?: UserItemJson[]; + userMusicDetailList?: UserMusicDetailJson[]; + userActivityList?: UserActivityJson[]; + userRecentRatingList?: UserRecentRatingJson[]; + userPlaylogList?: UserPlaylogJson[]; + userDataEx?: UserDataExJson[]; + + /** String of binary digits */ + isNewMapList: string; + + /** String of binary digits */ + isNewCharacterList: string; + + /** String of binary digits */ + isNewMusicDetailList: string; + + /** String of binary digits */ + isNewItemList: string; + + /** String of binary digits */ + isNewCourseList: string; + + /** String of binary digits */ + isNewDuelList: string; + }; +} diff --git a/src/chunithm/response/gameLogin.ts b/src/chunithm/response/gameLogin.ts new file mode 100644 index 0000000..09a419a --- /dev/null +++ b/src/chunithm/response/gameLogin.ts @@ -0,0 +1,4 @@ +export interface GameLoginResponse { + /** Integer */ + returnCode: string; +} diff --git a/src/chunithm/response/gameLogout.ts b/src/chunithm/response/gameLogout.ts new file mode 100644 index 0000000..63c4500 --- /dev/null +++ b/src/chunithm/response/gameLogout.ts @@ -0,0 +1,4 @@ +export interface GameLogoutResponse { + /** Integer */ + returnCode: string; +} diff --git a/src/chunithm/response/getGameCharge.ts b/src/chunithm/response/getGameCharge.ts new file mode 100644 index 0000000..8b1d209 --- /dev/null +++ b/src/chunithm/response/getGameCharge.ts @@ -0,0 +1,9 @@ +import { GameChargeJson } from "../proto/gameCharge"; + +export interface GetGameChargeResponse { + /** Integer */ + length: string; + + /** TBD */ + gameChargeList: GameChargeJson[]; +} diff --git a/src/chunithm/response/getGameEvent.ts b/src/chunithm/response/getGameEvent.ts new file mode 100644 index 0000000..0162e5e --- /dev/null +++ b/src/chunithm/response/getGameEvent.ts @@ -0,0 +1,11 @@ +import { GameEventJson } from "../proto/gameEvent"; + +export interface GetGameEventResponse { + /** Integer */ + type: string; + + /** Integer, number of results returned */ + length: string; + + gameEventList: GameEventJson[]; +} diff --git a/src/chunithm/response/getGameIdlist.ts b/src/chunithm/response/getGameIdlist.ts new file mode 100644 index 0000000..3988acc --- /dev/null +++ b/src/chunithm/response/getGameIdlist.ts @@ -0,0 +1,10 @@ +export interface GetGameIdlistResponse { + /** Integer */ + type: string; + + /** Integer */ + length: string; + + /** Format TBD */ + gameIdlistList: []; +} diff --git a/src/chunithm/response/getGameMessage.ts b/src/chunithm/response/getGameMessage.ts new file mode 100644 index 0000000..bce1f3d --- /dev/null +++ b/src/chunithm/response/getGameMessage.ts @@ -0,0 +1,11 @@ +import { GameMessageJson } from "../proto/gameMessage"; + +export interface GetGameMessageResponse { + /** Integer */ + type: string; + + /** Integer */ + length: string; + + gameMessageList: GameMessageJson[]; +} diff --git a/src/chunithm/response/getGameRanking.ts b/src/chunithm/response/getGameRanking.ts new file mode 100644 index 0000000..d2b28ae --- /dev/null +++ b/src/chunithm/response/getGameRanking.ts @@ -0,0 +1,10 @@ +import { GameRankingJson } from "../proto/gameRanking"; + +export interface GetGameRankingResponse { + /** Long */ + type: string; + + // No length parameter..? + + gameRankingList: GameRankingJson[]; +} diff --git a/src/chunithm/response/getGameSale.ts b/src/chunithm/response/getGameSale.ts new file mode 100644 index 0000000..5c42941 --- /dev/null +++ b/src/chunithm/response/getGameSale.ts @@ -0,0 +1,11 @@ +import { GameSaleJson } from "../proto/gameSale"; + +export interface GetGameSaleResponse { + /** Integer */ + type: string; + + /** Integer */ + length: string; + + gameSaleList: GameSaleJson[]; +} diff --git a/src/chunithm/response/getGameSetting.ts b/src/chunithm/response/getGameSetting.ts new file mode 100644 index 0000000..66aefd1 --- /dev/null +++ b/src/chunithm/response/getGameSetting.ts @@ -0,0 +1,32 @@ +export interface GetGameSettingResponse { + gameSetting: { + /** ROM version string e.g. "1.30.00" */ + dataVersion: string; + + isMaintenance: string; + + requestInterval: string; + + rebootStartTime: string; + + rebootEndTime: string; + + /** Boolean */ + isBackgroundDistribute: string; + + /** Integer, pagination granularity for GetUserCharacter */ + maxCountCharacter: string; + + /** Integer, pagination granularity for GetUserJson */ + maxCountItem: string; + + /** Integer, pagination granularity for GetUserMusic */ + maxCountMusic: string; + }; + + /** Boolean */ + isDumpUpload: string; + + /** Boolean */ + isAou: string; +} diff --git a/src/chunithm/response/getUserActivity.ts b/src/chunithm/response/getUserActivity.ts new file mode 100644 index 0000000..806617c --- /dev/null +++ b/src/chunithm/response/getUserActivity.ts @@ -0,0 +1,14 @@ +import { UserActivityJson } from "../proto/userActivity"; + +export interface GetUserActivityResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + /** Integer, round-trip from request */ + kind: string; + + userActivityList: UserActivityJson[]; +} diff --git a/src/chunithm/response/getUserCharacter.ts b/src/chunithm/response/getUserCharacter.ts new file mode 100644 index 0000000..a1cd8c6 --- /dev/null +++ b/src/chunithm/response/getUserCharacter.ts @@ -0,0 +1,17 @@ +import { UserCharacterJson } from "../proto/userCharacter"; + +export interface GetUserCharacterResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + /** + * Integer, pagination cookie. Sent back in next request if + * `length === maxCount`. + */ + nextIndex: string; + + userCharacterList: UserCharacterJson[]; +} diff --git a/src/chunithm/response/getUserCharge.ts b/src/chunithm/response/getUserCharge.ts new file mode 100644 index 0000000..80944a6 --- /dev/null +++ b/src/chunithm/response/getUserCharge.ts @@ -0,0 +1,10 @@ +export interface GetUserChargeResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + /** TBD */ + userChargeList: []; +} diff --git a/src/chunithm/response/getUserCourse.ts b/src/chunithm/response/getUserCourse.ts new file mode 100644 index 0000000..b9a9296 --- /dev/null +++ b/src/chunithm/response/getUserCourse.ts @@ -0,0 +1,13 @@ +export interface GetUserCourseResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + /** Integer, pagination cookie */ + nextIndex: string; + + /** TBD */ + userCourseList: []; +} diff --git a/src/chunithm/response/getUserData.ts b/src/chunithm/response/getUserData.ts new file mode 100644 index 0000000..be08eb7 --- /dev/null +++ b/src/chunithm/response/getUserData.ts @@ -0,0 +1,8 @@ +import { UserDataJson } from "../proto/userData"; + +export interface GetUserDataResponse { + /** Integer, AiMe ID */ + userId: string; + + userData: UserDataJson; +} diff --git a/src/chunithm/response/getUserDataEx.ts b/src/chunithm/response/getUserDataEx.ts new file mode 100644 index 0000000..10690f4 --- /dev/null +++ b/src/chunithm/response/getUserDataEx.ts @@ -0,0 +1,8 @@ +import { UserDataExJson } from "../proto/userDataEx"; + +export interface GetUserDataExResponse { + /** Integer, AiMe ID */ + userId: string; + + userDataEx: UserDataExJson; +} diff --git a/src/chunithm/response/getUserDuel.ts b/src/chunithm/response/getUserDuel.ts new file mode 100644 index 0000000..e419ce5 --- /dev/null +++ b/src/chunithm/response/getUserDuel.ts @@ -0,0 +1,10 @@ +export interface GetUserDuelResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + /** TBD */ + userDuelList: []; +} diff --git a/src/chunithm/response/getUserItem.ts b/src/chunithm/response/getUserItem.ts new file mode 100644 index 0000000..b9a42e3 --- /dev/null +++ b/src/chunithm/response/getUserItem.ts @@ -0,0 +1,37 @@ +import { UserItemJson } from "../proto/userItem"; + +export interface GetUserItemResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + /** + * Long. + * + * The behavior of this field has been determined through black-box trial and + * error, not through any sort of systematic decompilation. However, it seems + * that it can be characterized as follows: + * + * - If no further records of this `kind` are available then a + * negative number should be returned. + * - If further records are available then return `kind` * `G` + `cookie`, + * where + * - `kind` is the `UserItemJson.kind` value extracted from the request + * - `G` is ten billion (smallest power of ten greater than 2**32) + * - `cookie` is probably any 32-bit integer context value you want + * + * @see GetUserJsonRequest + */ + nextIndex: string; + + /** + * Integer. Item kind, must match the `kind` value extracted from the + * request in the manner described by the `GetUserJsonRequest` docs. + */ + itemKind: string; + + /** List of items of the requested `kind` only */ + userItemList: UserItemJson[]; +} diff --git a/src/chunithm/response/getUserMap.ts b/src/chunithm/response/getUserMap.ts new file mode 100644 index 0000000..9519128 --- /dev/null +++ b/src/chunithm/response/getUserMap.ts @@ -0,0 +1,11 @@ +import { UserMapJson } from "../proto/userMap"; + +export interface GetUserMapResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + userMapList: UserMapJson[]; +} diff --git a/src/chunithm/response/getUserMusic.ts b/src/chunithm/response/getUserMusic.ts new file mode 100644 index 0000000..c1d0815 --- /dev/null +++ b/src/chunithm/response/getUserMusic.ts @@ -0,0 +1,21 @@ +import { UserMusicJson } from "../proto/userMusic"; + +export interface GetUserMusicResponse { + /** Integer, AiMe ID */ + userId: string; + + /** + * Integer, number of top-level `UserMusicJson` objects returned (each of + * which will, in turn, contain one or more "detail" objects). + */ + length: string; + + /** Integer, pagination cookie */ + nextIndex: string; + + /** + * List of `UserMusicDetailJson` objects grouped by `musicId` into + * `UserMusicJson` objects. + **/ + userMusicList: UserMusicJson[]; +} diff --git a/src/chunithm/response/getUserOption.ts b/src/chunithm/response/getUserOption.ts new file mode 100644 index 0000000..e1e4769 --- /dev/null +++ b/src/chunithm/response/getUserOption.ts @@ -0,0 +1,8 @@ +import { UserGameOptionJson } from "../proto/userGameOption"; + +export interface GetUserOptionResponse { + /** Integer, AiMe ID */ + userId: string; + + userGameOption: UserGameOptionJson; +} diff --git a/src/chunithm/response/getUserOptionEx.ts b/src/chunithm/response/getUserOptionEx.ts new file mode 100644 index 0000000..5e1f33b --- /dev/null +++ b/src/chunithm/response/getUserOptionEx.ts @@ -0,0 +1,8 @@ +import { UserGameOptionExJson } from "../proto/userGameOptionEx"; + +export interface GetUserOptionExResponse { + /** Integer, AiMe ID */ + userId: string; + + userGameOptionEx: UserGameOptionExJson; +} diff --git a/src/chunithm/response/getUserPreview.ts b/src/chunithm/response/getUserPreview.ts new file mode 100644 index 0000000..de0f6c0 --- /dev/null +++ b/src/chunithm/response/getUserPreview.ts @@ -0,0 +1,68 @@ +import { UserCharacterJson } from "../proto/userCharacter"; + +export interface GetUserPreviewResponse { + // + // AiMe ID, from request + // + + /** Integer */ + userId: string; + + // + // Current login (i.e. profile lock) status + // + + /** Boolean */ + isLogin: string; + + /** Date "YYYY-MM-DD hh:mm:ss" */ + lastLoginDate: string; + + // + // UserData + // + + /** String */ + userName: string; + + /** Integer */ + reincarnationNum: string; + + /** Integer */ + level: string; + + /** Integer */ + exp: string; + + /** Integer */ + playerRating: string; + + /** String */ + lastGameId: string; + + /** String */ + lastRomVersion: string; + + /** String */ + lastDataVersion: string; + + /** Date "YYYY-MM-DD hh:mm:ss" */ + lastPlayDate: string; + + /** Integer */ + trophyId: string; + + // + // Currently selected UserCharacter + // + + userCharacter: UserCharacterJson; + + // + // UserGameOption + // + + playerLevel: string; + rating: string; + headphone: string; +} diff --git a/src/chunithm/response/getUserRecentRating.ts b/src/chunithm/response/getUserRecentRating.ts new file mode 100644 index 0000000..ca0972d --- /dev/null +++ b/src/chunithm/response/getUserRecentRating.ts @@ -0,0 +1,11 @@ +import { UserRecentRatingJson } from "../proto/userRecentRating"; + +export interface GetUserRecentRatingResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + userRecentRatingList: UserRecentRatingJson[]; +} diff --git a/src/chunithm/response/getUserRegion.ts b/src/chunithm/response/getUserRegion.ts new file mode 100644 index 0000000..01ac93a --- /dev/null +++ b/src/chunithm/response/getUserRegion.ts @@ -0,0 +1,11 @@ +import { UserRegionJson } from "../proto/userRegion"; + +export interface GetUserRegionResponse { + /** Integer, AiMe ID */ + userId: string; + + /** Integer, number of results returned */ + length: string; + + userRegionList: UserRegionJson[]; +} diff --git a/src/chunithm/response/upsertClientBookkeeping.ts b/src/chunithm/response/upsertClientBookkeeping.ts new file mode 100644 index 0000000..b8280b4 --- /dev/null +++ b/src/chunithm/response/upsertClientBookkeeping.ts @@ -0,0 +1,3 @@ +export interface UpsertClientBookkeepingResponse { + returnCode: string; +} diff --git a/src/chunithm/response/upsertClientDevelop.ts b/src/chunithm/response/upsertClientDevelop.ts new file mode 100644 index 0000000..12c0dc1 --- /dev/null +++ b/src/chunithm/response/upsertClientDevelop.ts @@ -0,0 +1,4 @@ +export interface UpsertClientDevelopResponse { + /** Integer */ + returnCode: string; +} diff --git a/src/chunithm/response/upsertClientError.ts b/src/chunithm/response/upsertClientError.ts new file mode 100644 index 0000000..db31c16 --- /dev/null +++ b/src/chunithm/response/upsertClientError.ts @@ -0,0 +1,4 @@ +export interface UpsertClientErrorResponse { + /** Integer */ + returnCode: string; +} diff --git a/src/chunithm/response/upsertClientSetting.ts b/src/chunithm/response/upsertClientSetting.ts new file mode 100644 index 0000000..a7beed7 --- /dev/null +++ b/src/chunithm/response/upsertClientSetting.ts @@ -0,0 +1,4 @@ +export interface UpsertClientSettingResponse { + /** Integer */ + returnCode: string; +} diff --git a/src/chunithm/response/upsertClientTestmode.ts b/src/chunithm/response/upsertClientTestmode.ts new file mode 100644 index 0000000..5971d32 --- /dev/null +++ b/src/chunithm/response/upsertClientTestmode.ts @@ -0,0 +1,4 @@ +export interface UpsertClientTestmodeResponse { + /** Integer */ + returnCode: string; +} diff --git a/src/chunithm/response/upsertUserAll.ts b/src/chunithm/response/upsertUserAll.ts new file mode 100644 index 0000000..f4557c4 --- /dev/null +++ b/src/chunithm/response/upsertUserAll.ts @@ -0,0 +1,4 @@ +export interface UpsertUserAllResponse { + /** Integer */ + returnCode: string; +} diff --git a/src/chunithm/rpc.ts b/src/chunithm/rpc.ts new file mode 100644 index 0000000..c434ddc --- /dev/null +++ b/src/chunithm/rpc.ts @@ -0,0 +1,21 @@ +import { RequestHandler } from "express"; + +import { Repositories } from "./repo"; + +/** + * An async function that is supplied with an implementation of the + * `Repositories` instance and uses it to transform a JSON request into a + * JSON response. + */ +export type RpcHandler = (rep: Repositories, reqObj: Q) => Promise; + +/** + * This describes a piece of router-like Express middleware that provides the + * connective tissue between an HTTP request scope and the life cycle of an + * implementation of the `Repositories` persistent data storage interface. Call + * `rpc()` to declare async JSON RPC handlers, then once that's fully prepared + * you can pass the implementation of this type to Express. + */ +export type RpcWrapper = RequestHandler & { + rpc: (path: string, handler: RpcHandler) => RpcWrapper; +}; diff --git a/src/chunithm/sql/_factory.ts b/src/chunithm/sql/_factory.ts new file mode 100644 index 0000000..d6d85d4 --- /dev/null +++ b/src/chunithm/sql/_factory.ts @@ -0,0 +1,66 @@ +import { SqlUserActivityRepository } from "./userActivity"; +import { SqlUserCharacterRepository } from "./userCharacter"; +import { SqlUserDataRepository } from "./userData"; +import { SqlUserDataExRepository } from "./userDataEx"; +import { SqlUserGameOptionRepository } from "./userGameOption"; +import { SqlUserGameOptionExRepository } from "./userGameOptionEx"; +import { SqlUserItemRepository } from "./userItem"; +import { SqlUserMapRepository } from "./userMap"; +import { SqlUserMusicRepository } from "./userMusic"; +import { SqlUserPlaylogRepository } from "./userPlaylog"; +import { Repositories } from "../repo"; +import { UserActivityRepository } from "../repo/userActivity"; +import { UserCharacterRepository } from "../repo/userCharacter"; +import { UserDataRepository } from "../repo/userData"; +import { UserDataExRepository } from "../repo/userDataEx"; +import { UserGameOptionRepository } from "../repo/userGameOption"; +import { UserGameOptionExRepository } from "../repo/userGameOptionEx"; +import { UserItemRepository } from "../repo/userItem"; +import { UserMapRepository } from "../repo/userMap"; +import { UserMusicRepository } from "../repo/userMusic"; +import { UserPlaylogRepository } from "../repo/userPlaylog"; +import { Transaction } from "../../sql"; + +export class SqlRepositories implements Repositories { + constructor(private readonly _txn: Transaction) {} + + userActivity(): UserActivityRepository { + return new SqlUserActivityRepository(this._txn); + } + + userCharacter(): UserCharacterRepository { + return new SqlUserCharacterRepository(this._txn); + } + + userData(): UserDataRepository { + return new SqlUserDataRepository(this._txn); + } + + userDataEx(): UserDataExRepository { + return new SqlUserDataExRepository(this._txn); + } + + userGameOption(): UserGameOptionRepository { + return new SqlUserGameOptionRepository(this._txn); + } + + userGameOptionEx(): UserGameOptionExRepository { + return new SqlUserGameOptionExRepository(this._txn); + } + + userItem(): UserItemRepository { + return new SqlUserItemRepository(this._txn); + } + + userMap(): UserMapRepository { + return new SqlUserMapRepository(this._txn); + } + + userMusic(): UserMusicRepository { + return new SqlUserMusicRepository(this._txn); + } + + userPlaylog(): UserPlaylogRepository { + return new SqlUserPlaylogRepository(this._txn); + } +} diff --git a/src/chunithm/sql/index.ts b/src/chunithm/sql/index.ts new file mode 100644 index 0000000..4e9422b --- /dev/null +++ b/src/chunithm/sql/index.ts @@ -0,0 +1,55 @@ +import logger from "debug"; +import { NextFunction, Router, Request, Response } from "express"; + +import { SqlRepositories } from "./_factory"; +import { RpcHandler, RpcWrapper } from "../rpc"; +import { DataSource } from "../../sql"; + +const debug = logger("app:chuni:handler"); + +export default function createSqlWrapper(db: DataSource): RpcWrapper { + // We return a piece of middleware that delegates to an Express router + + const router = Router(); + + // The ES6 class syntax is nice, but it lacks an `operator()` syntax, so we + // have to do this the confusing and messy way. Declare an inner function and + // then add "method" fields to the resulting function object. + + function self(req: Request, res: Response, next: NextFunction) { + // Forward all requests to our embedded Express router + return router(req, res, next); + } + + self.rpc = function(path: string, handler: RpcHandler) { + // Declare the RPC as a POST endpoint on the Express router as follows: + router.post(path, async function(req, res) { + // When you receive an HTTP request on this path... + try { + // return the JSON response returned by... + res.json( + // ... the provided handler, executing within an SQL transaction. + await db.transaction(txn => + handler(new SqlRepositories(txn), req.body) + ) + ); + } catch (e) { + // .. unless something goes wrong of course + if (debug.enabled) { + debug("Error processing request: %s", e.stack); + } + + res.sendStatus(500); + } + }); + + // Allow .rpc() calls to be chained, since that's what Express' native + // .post() etc methods do. + + return self; + }; + + // Done defining our Express middleware, so here it is: + + return self; +} diff --git a/src/chunithm/sql/userActivity.ts b/src/chunithm/sql/userActivity.ts new file mode 100644 index 0000000..5897078 --- /dev/null +++ b/src/chunithm/sql/userActivity.ts @@ -0,0 +1,50 @@ +import sql from "sql-bricks-postgres"; + +import { UserActivityItem } from "../model/userActivity"; +import { UserDataItem } from "../model/userData"; +import { UserActivityRepository } from "../repo/userActivity"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + kind: T.number, + activityId: T.number, // This is just called "id" on the wire + sortNumber: T.number, + param1: T.number, + param2: T.number, + param3: T.number, + param4: T.number, +}); + +export class SqlUserActivityRepository implements UserActivityRepository { + constructor(private readonly _txn: Transaction) {} + + async load( + profileId: Id, + kind: number + ): Promise { + const stmt = sql + .select("*") + .from("cm_user_activity") + .where("profile_id", profileId) + .where("kind", kind); + + const rows = await this._txn.fetchRows(stmt); + + return rows.map(readRow); + } + + save(profileId: Id, obj: UserActivityItem): Promise { + const stmt = sql + .insert("cm_user_activity", { + id: this._txn.generateId(), + profile_id: profileId, + ...writeRow(obj), + }) + .onConflict("profile_id", "kind", "activity_id") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userCharacter.ts b/src/chunithm/sql/userCharacter.ts new file mode 100644 index 0000000..82c3507 --- /dev/null +++ b/src/chunithm/sql/userCharacter.ts @@ -0,0 +1,75 @@ +import sql from "sql-bricks-postgres"; + +import { UserCharacterItem } from "../model/userCharacter"; +import { UserDataItem } from "../model/userData"; +import { Page } from "../repo"; +import { UserCharacterRepository } from "../repo/userCharacter"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + characterId: T.number, + playCount: T.number, + level: T.number, + skillId: T.number, + friendshipExp: T.number, + isValid: T.boolean, + isNewMark: T.boolean, + param1: T.number, + param2: T.number, +}); + +export class SqlUserCharacterRepository implements UserCharacterRepository { + constructor(private readonly _txn: Transaction) {} + + async load( + profileId: Id, + page?: Page | undefined + ): Promise { + const stmt = sql + .select("c.*") + .from("cm_user_character c") + .where("c.profile_id", profileId); + + if (page) { + stmt.limit(page.limit).offset(page.offset); + } + + const rows = await this._txn.fetchRows(stmt); + + return rows.map(readRow); + } + + async loadOne( + profileId: Id, + characterId: number + ): Promise { + const stmt = sql + .select("c.*") + .from("cm_user_character c") + .where("c.profile_id", profileId) + .where("c.character_id", characterId); + + const row = await this._txn.fetchRow(stmt); + + if (row === undefined) { + throw new Error("Database corrupted: selected character not found"); + } + + return readRow(row); + } + + save(profileId: Id, obj: UserCharacterItem): Promise { + const stmt = sql + .insert("cm_user_character", { + id: this._txn.generateId(), + profile_id: profileId, + ...writeRow(obj), + }) + .onConflict("profile_id", "character_id") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userData.ts b/src/chunithm/sql/userData.ts new file mode 100644 index 0000000..a8d4138 --- /dev/null +++ b/src/chunithm/sql/userData.ts @@ -0,0 +1,137 @@ +import sql from "sql-bricks-postgres"; + +import { UserDataItem } from "../model/userData"; +import { UserDataRepository } from "../repo/userData"; +import { AimeId, Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow } = createSqlMapper({ + accessCode: T.string, + userName: T.string, + isWebJoin: T.boolean, + webLimitDate: T.Date, + level: T.number, + reincarnationNum: T.number, + exp: T.number, + point: T.number, + totalPoint: T.bigint, + playCount: T.number, + multiPlayCount: T.number, + multiWinCount: T.number, + requestResCount: T.number, + acceptResCount: T.number, + successResCount: T.number, + playerRating: T.number, + highestRating: T.number, + nameplateId: T.number, + frameId: T.number, + characterId: T.number, + trophyId: T.number, + playedTutorialBit: T.number, + firstTutorialCancelNum: T.number, + masterTutorialCancelNum: T.number, + totalRepertoireCount: T.number, + totalMapNum: T.number, + totalHiScore: T.bigint, + totalBasicHighScore: T.bigint, + totalAdvancedHighScore: T.bigint, + totalExpertHighScore: T.bigint, + totalMasterHighScore: T.bigint, + eventWatchedDate: T.Date, + friendCount: T.number, + isMaimai: T.boolean, + firstGameId: T.string, + firstRomVersion: T.string, + firstDataVersion: T.string, + firstPlayDate: T.Date, + lastGameId: T.string, + lastRomVersion: T.string, + lastDataVersion: T.string, + lastPlayDate: T.Date, + lastPlaceId: T.number, + lastPlaceName: T.string, + lastRegionId: T.string, + lastRegionName: T.string, + lastAllNetId: T.string, + lastClientId: T.string, +}); + +export class SqlUserDataRepository implements UserDataRepository { + constructor(private readonly _txn: Transaction) {} + + async tryLookup(aimeId: AimeId): Promise | undefined> { + const stmt = sql + .select("d.id") + .from("cm_user_data d") + .join("aime_player p", { "d.player_id": "p.id" }) + .where("p.ext_id", aimeId); + + const row = await this._txn.fetchRow(stmt); + + return row && (row.id as Id); + } + + async lookup(aimeId: AimeId): Promise> { + const id = await this.tryLookup(aimeId); + + if (id === undefined) { + throw new Error("AiMe ID not found"); + } + + return id; + } + + async load(dataId: Id): Promise { + const stmt = sql + .select("*") + .from("cm_user_data") + .where("id", dataId); + + const row = await this._txn.fetchRow(stmt); + + if (row === undefined) { + throw new Error("UserData record not found"); + } + + return readRow(row); + } + + async save(aimeId: AimeId, obj: UserDataItem): Promise> { + const lookupStmt = sql + .select("p.id as player_id", "d.id as data_id") + .from("aime_player p") + .leftJoin("cm_user_data d", { "p.id": "d.player_id" }) + .where("p.ext_id", aimeId); + + const row = await this._txn.fetchRow(lookupStmt); + + if (row === undefined) { + throw new Error("AiMe ID not found"); + } + + const playerId = row.player_id; + const existingId = row.data_id as Id; + + if (existingId !== null) { + const stmt = sql + .update("cm_user_data", writeRow(obj)) + .where("id", existingId); + + await this._txn.modify(stmt); + + return existingId; + } else { + const id = this._txn.generateId(); + const insertStmt = sql.insert("cm_user_data", { + id: id, + player_id: playerId, + ...writeRow(obj), + }); + + await this._txn.modify(insertStmt); + + return id as Id; + } + } +} diff --git a/src/chunithm/sql/userDataEx.ts b/src/chunithm/sql/userDataEx.ts new file mode 100644 index 0000000..5f116f2 --- /dev/null +++ b/src/chunithm/sql/userDataEx.ts @@ -0,0 +1,76 @@ +import sql from "sql-bricks-postgres"; + +import { UserDataItem } from "../model/userData"; +import { UserDataExItem } from "../model/userDataEx"; +import { UserDataExRepository } from "../repo/userDataEx"; +import { AimeId, Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + compatibleCmVersion: T.string, + medal: T.number, + mapIconId: T.number, + voiceId: T.number, + ext1: T.number, + ext2: T.number, + ext3: T.number, + ext4: T.number, + ext5: T.number, + ext6: T.number, + ext7: T.number, + ext8: T.number, + ext9: T.number, + ext10: T.number, + ext11: T.number, + ext12: T.number, + ext13: T.number, + ext14: T.number, + ext15: T.number, + ext16: T.number, + ext17: T.number, + ext18: T.number, + ext19: T.number, + ext20: T.number, + extStr1: T.string, + extStr2: T.string, + extStr3: T.string, + extStr4: T.string, + extStr5: T.string, + extLong1: T.bigint, + extLong2: T.bigint, + extLong3: T.bigint, + extLong4: T.bigint, + extLong5: T.bigint, +}); + +export class SqlUserDataExRepository implements UserDataExRepository { + constructor(private readonly _txn: Transaction) {} + + async load(dataId: Id): Promise { + const stmt = sql + .select("*") + .from("cm_user_data_ex") + .where("id", dataId); + + const row = await this._txn.fetchRow(stmt); + + if (row === undefined) { + throw new Error("UserDataEx record not found"); + } + + return readRow(row); + } + + async save(profileId: Id, obj: UserDataExItem): Promise { + const stmt = sql + .insert("cm_user_data_ex", { + id: profileId, + ...writeRow(obj), + }) + .onConflict("id") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userGameOption.ts b/src/chunithm/sql/userGameOption.ts new file mode 100644 index 0000000..17de7f0 --- /dev/null +++ b/src/chunithm/sql/userGameOption.ts @@ -0,0 +1,64 @@ +import sql from "sql-bricks-postgres"; + +import { UserDataItem } from "../model/userData"; +import { UserGameOptionItem } from "../model/userGameOption"; +import { UserGameOptionRepository } from "../repo/userGameOption"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + bgInfo: T.number, + fieldColor: T.number, + guideSound: T.number, + soundEffect: T.number, + guideLine: T.number, + speed: T.number, + optionSet: T.number, + matching: T.number, + judgePos: T.number, + rating: T.number, + judgeJustice: T.number, + judgeAttack: T.number, + headphone: T.number, + playerLevel: T.number, + successTap: T.number, + successExTap: T.number, + successSlideHold: T.number, + successAir: T.number, + successFlick: T.number, + successSkill: T.number, + successTapTimbre: T.number, + privacy: T.number, +}); + +export class SqlUserGameOptionRepository implements UserGameOptionRepository { + constructor(private readonly _txn: Transaction) {} + + async load(dataId: Id): Promise { + const stmt = sql + .select("*") + .from("cm_user_game_option") + .where("id", dataId); + + const row = await this._txn.fetchRow(stmt); + + if (row === undefined) { + throw new Error("UserGameOption record not found"); + } + + return readRow(row); + } + + save(profileId: Id, obj: UserGameOptionItem): Promise { + const stmt = sql + .insert("cm_user_game_option", { + id: profileId, + ...writeRow(obj), + }) + .onConflict("id") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userGameOptionEx.ts b/src/chunithm/sql/userGameOptionEx.ts new file mode 100644 index 0000000..d63e828 --- /dev/null +++ b/src/chunithm/sql/userGameOptionEx.ts @@ -0,0 +1,63 @@ +import sql from "sql-bricks-postgres"; + +import { UserDataItem } from "../model/userData"; +import { UserGameOptionExItem } from "../model/userGameOptionEx"; +import { UserGameOptionExRepository } from "../repo/userGameOptionEx"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + ext1: T.number, + ext2: T.number, + ext3: T.number, + ext4: T.number, + ext5: T.number, + ext6: T.number, + ext7: T.number, + ext8: T.number, + ext9: T.number, + ext10: T.number, + ext11: T.number, + ext12: T.number, + ext13: T.number, + ext14: T.number, + ext15: T.number, + ext16: T.number, + ext17: T.number, + ext18: T.number, + ext19: T.number, + ext20: T.number, +}); + +export class SqlUserGameOptionExRepository + implements UserGameOptionExRepository { + constructor(private readonly _txn: Transaction) {} + + async load(dataId: Id): Promise { + const stmt = sql + .select("*") + .from("cm_user_game_option_ex") + .where("id", dataId); + + const row = await this._txn.fetchRow(stmt); + + if (row === undefined) { + throw new Error("UserGameOptionEx record not found"); + } + + return readRow(row); + } + + save(profileId: Id, obj: UserGameOptionExItem): Promise { + const stmt = sql + .insert("cm_user_game_option_ex", { + id: profileId, + ...writeRow(obj), + }) + .onConflict("id") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userItem.ts b/src/chunithm/sql/userItem.ts new file mode 100644 index 0000000..4ab0a72 --- /dev/null +++ b/src/chunithm/sql/userItem.ts @@ -0,0 +1,53 @@ +import sql from "sql-bricks-postgres"; + +import { UserDataItem } from "../model/userData"; +import { UserItemItem } from "../model/userItem"; +import { Page } from "../repo"; +import { UserItemRepository } from "../repo/userItem"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + itemKind: T.number, + itemId: T.number, + stock: T.number, + isValid: T.boolean, +}); + +export class SqlUserItemRepository implements UserItemRepository { + constructor(private readonly _txn: Transaction) {} + + async load( + profileId: Id, + itemKind: number, + page?: Page + ): Promise { + const stmt = sql + .select("*") + .from("cm_user_item") + .where("profile_id", profileId) + .where("item_kind", itemKind); + + if (page) { + stmt.limit(page.limit).offset(page.offset); + } + + const rows = await this._txn.fetchRows(stmt); + + return rows.map(readRow); + } + + save(profileId: Id, obj: UserItemItem): Promise { + const stmt = sql + .insert("cm_user_item", { + id: this._txn.generateId(), + profile_id: profileId, + ...writeRow(obj), + }) + .onConflict("profile_id", "item_kind", "item_id") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userMap.ts b/src/chunithm/sql/userMap.ts new file mode 100644 index 0000000..e086285 --- /dev/null +++ b/src/chunithm/sql/userMap.ts @@ -0,0 +1,56 @@ +import sql from "sql-bricks-postgres"; + +import { UserDataItem } from "../model/userData"; +import { UserMapItem } from "../model/userMap"; +import { Page } from "../repo"; +import { UserMapRepository } from "../repo/userMap"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + mapId: T.number, + position: T.number, + isClear: T.boolean, + areaId: T.number, + routeNumber: T.number, + eventId: T.number, + rate: T.number, + statusCount: T.number, + isValid: T.boolean, +}); + +export class SqlUserMapRepository implements UserMapRepository { + constructor(private readonly _txn: Transaction) {} + + async load( + profileId: Id, + page?: Page + ): Promise { + const stmt = sql + .select("*") + .from("cm_user_map") + .where("profile_id", profileId); + + if (page) { + stmt.limit(page.limit).offset(page.offset); + } + + const rows = await this._txn.fetchRows(stmt); + + return rows.map(readRow); + } + + save(profileId: Id, obj: UserMapItem): Promise { + const stmt = sql + .insert("cm_user_map", { + id: this._txn.generateId(), + profile_id: profileId, + ...writeRow(obj), + }) + .onConflict("profile_id", "map_id") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userMusic.ts b/src/chunithm/sql/userMusic.ts new file mode 100644 index 0000000..037a3a9 --- /dev/null +++ b/src/chunithm/sql/userMusic.ts @@ -0,0 +1,63 @@ +import sql from "sql-bricks-postgres"; + +import { UserDataItem } from "../model/userData"; +import { UserMusicDetailItem } from "../model/userMusic"; +import { Page } from "../repo"; +import { UserMusicRepository } from "../repo/userMusic"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { readRow, writeRow, colNames } = createSqlMapper({ + musicId: T.number, + level: T.number, + playCount: T.number, + scoreMax: T.number, + resRequestCount: T.number, + resAcceptCount: T.number, + resSuccessCount: T.number, + missCount: T.number, + maxComboCount: T.number, + isFullCombo: T.boolean, + isAllJustice: T.boolean, + isSuccess: T.boolean, + fullChain: T.number, + maxChain: T.number, + scoreRank: T.number, + isLock: T.boolean, +}); + +export class SqlUserMusicRepository implements UserMusicRepository { + constructor(private readonly _txn: Transaction) {} + + async load( + profileId: Id, + page?: Page + ): Promise { + const stmt = sql + .select("*") + .from("cm_user_music") + .where("profile_id", profileId); + + if (page) { + stmt.limit(page.limit).offset(page.offset); + } + + const rows = await this._txn.fetchRows(stmt); + + return rows.map(readRow); + } + + save(profileId: Id, obj: UserMusicDetailItem): Promise { + const stmt = sql + .insert("cm_user_music", { + id: this._txn.generateId(), + profile_id: profileId, + ...writeRow(obj), + }) + .onConflict("profile_id", "music_id", "level") + .doUpdate(colNames); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/sql/userPlaylog.ts b/src/chunithm/sql/userPlaylog.ts new file mode 100644 index 0000000..c377701 --- /dev/null +++ b/src/chunithm/sql/userPlaylog.ts @@ -0,0 +1,75 @@ +import sql from "sql-bricks-postgres"; + +import { Id } from "../../model"; +import { UserDataItem } from "../model/userData"; +import { UserPlaylogItem } from "../model/userPlaylog"; +import { UserPlaylogRepository } from "../repo/userPlaylog"; +import { Transaction } from "../../sql"; +import { T, createSqlMapper } from "../../sql/util"; + +const { writeRow } = createSqlMapper({ + orderId: T.number, + sortNumber: T.number, + placeId: T.number, + playDate: T.Date, + userPlayDate: T.Date, + musicId: T.number, + level: T.number, + customId: T.number, + playedUserId1: T.number, + playedUserId2: T.number, + playedUserId3: T.number, + playedUserName1: T.string, + playedUserName2: T.string, + playedUserName3: T.string, + playedMusicLevel1: T.number, + playedMusicLevel2: T.number, + playedMusicLevel3: T.number, + playedCustom1: T.number, + playedCustom2: T.number, + playedCustom3: T.number, + track: T.number, + score: T.number, + rank: T.number, + maxCombo: T.number, + maxChain: T.number, + rateTap: T.number, + rateHold: T.number, + rateSlide: T.number, + rateAir: T.number, + rateFlick: T.number, + judgeGuilty: T.number, + judgeAttack: T.number, + judgeJustice: T.number, + judgeCritical: T.number, + eventId: T.number, + playerRating: T.number, + isNewRecord: T.boolean, + isFullCombo: T.boolean, + fullChainKind: T.number, + isAllJustice: T.boolean, + isContinue: T.boolean, + isFreeToPlay: T.boolean, + characterId: T.number, + skillId: T.number, + playKind: T.number, + isClear: T.boolean, + skillLevel: T.number, + skillEffect: T.number, + placeName: T.string, + isMaimai: T.boolean, +}); + +export class SqlUserPlaylogRepository implements UserPlaylogRepository { + constructor(private readonly _txn: Transaction) {} + + save(profileId: Id, obj: UserPlaylogItem): Promise { + const stmt = sql.insert("cm_user_playlog", { + id: this._txn.generateId(), + profile_id: profileId, + ...writeRow(obj), + }); + + return this._txn.modify(stmt); + } +} diff --git a/src/chunithm/upsertClientBookkeeping.ts b/src/chunithm/upsertClientBookkeeping.ts deleted file mode 100644 index 9ed2053..0000000 --- a/src/chunithm/upsertClientBookkeeping.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Request, Response } from "express"; - -export default function upsertClientBookkeeping(req: Request, res: Response) { - res.json({ - returnCode: 1, - apiName: "UpsertClientBookkeepingApi", - }); -} diff --git a/src/chunithm/upsertClientDevelop.ts b/src/chunithm/upsertClientDevelop.ts deleted file mode 100644 index 2b92175..0000000 --- a/src/chunithm/upsertClientDevelop.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Request, Response } from "express"; - -export default function upsertClientDevelop(req: Request, res: Response) { - res.json({ - returnCode: 1, - apiName: "UpsertClientDevelopApi", - }); -} diff --git a/src/chunithm/upsertClientError.ts b/src/chunithm/upsertClientError.ts deleted file mode 100644 index 150fb7e..0000000 --- a/src/chunithm/upsertClientError.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Request, Response } from "express"; - -export default function upsertClientError(req: Request, res: Response) { - res.json({ - returnCode: 1, - apiName: "UpsertClientErrorApi", - }); -} diff --git a/src/chunithm/upsertClientSetting.ts b/src/chunithm/upsertClientSetting.ts deleted file mode 100644 index b27f27b..0000000 --- a/src/chunithm/upsertClientSetting.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Request, Response } from "express"; - -export default function getGameSetting(req: Request, res: Response) { - res.json({ - returnCode: 1, - apiName: "UpsertClientSettingApi", - }); -} diff --git a/src/chunithm/upsertClientTestmode.ts b/src/chunithm/upsertClientTestmode.ts deleted file mode 100644 index 254578f..0000000 --- a/src/chunithm/upsertClientTestmode.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Request, Response } from "express"; - -export default function upsertClientTestmode(req: Request, res: Response) { - res.json({ - returnCode: 1, - apiName: "UpsertClientTestmodeApi", - }); -} diff --git a/src/index.ts b/src/index.ts index 31b886c..2943408 100644 --- a/src/index.ts +++ b/src/index.ts @@ -45,7 +45,7 @@ import * as Swb from "./switchboard"; http.createServer(allnet).listen(Swb.PORT_ALLNET, Swb.HOST_INT); https.createServer(tls, billing).listen(Swb.PORT_BILLING, Swb.HOST_INT); - http.createServer(chunithm).listen(Swb.PORT_CHUNITHM, Swb.HOST_INT); + http.createServer(chunithm(db)).listen(Swb.PORT_CHUNITHM, Swb.HOST_INT); http.createServer(diva).listen(Swb.PORT_DIVA, Swb.HOST_INT); net.createServer(idz(db)).listen(Swb.PORT_IDZ.USERDB.TCP, Swb.HOST_INT);