diff --git a/src/idz/decoder/createAutoTeam.ts b/src/idz/decoder/createAutoTeam.ts new file mode 100644 index 0000000..07f17f8 --- /dev/null +++ b/src/idz/decoder/createAutoTeam.ts @@ -0,0 +1,15 @@ +import { RequestCode } from "./_defs"; +import { CreateAutoTeamRequest } from "../request/createAutoTeam"; +import { AimeId } from "../../model"; + +createAutoTeam.msgCode = 0x007b as RequestCode; +createAutoTeam.msgLen = 0x0010; + +export function createAutoTeam(buf: Buffer): CreateAutoTeamRequest { + return { + type: "create_auto_team_req", + aimeId: buf.readUInt32LE(0x0004) as AimeId, + field_0008: buf.readUInt32LE(0x0008), + field_000C: buf.readUInt8(0x000c), + }; +} diff --git a/src/idz/decoder/index.ts b/src/idz/decoder/index.ts index d6f00cd..70d0161 100644 --- a/src/idz/decoder/index.ts +++ b/src/idz/decoder/index.ts @@ -3,7 +3,7 @@ import { Transform } from "stream"; import { checkTeamName } from "./checkTeamName"; import { createProfile } from "./createProfile"; import { createTeam } from "./createTeam"; -import { joinAutoTeam } from "./joinAutoTeam"; +import { createAutoTeam } from "./createAutoTeam"; import { discoverProfile } from "./discoverProfile"; import { load2on2_v1, load2on2_v2 } from "./load2on2"; import { loadConfig } from "./loadConfig"; @@ -54,9 +54,9 @@ export type ReaderFn = ((buf: Buffer) => Request) & { const funcList: ReaderFn[] = [ checkTeamName, + createAutoTeam, createProfile, createTeam, - joinAutoTeam, discoverProfile, load2on2_v1, load2on2_v2, diff --git a/src/idz/decoder/joinAutoTeam.ts b/src/idz/decoder/joinAutoTeam.ts deleted file mode 100644 index 0cc6de8..0000000 --- a/src/idz/decoder/joinAutoTeam.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { RequestCode } from "./_defs"; -import { JoinAutoTeamRequest } from "../request/joinAutoTeam"; - -joinAutoTeam.msgCode = 0x007b as RequestCode; -joinAutoTeam.msgLen = 0x0010; - -export function joinAutoTeam(buf: Buffer): JoinAutoTeamRequest { - return { - type: "join_auto_team_req", - field_0004: buf.readUInt32LE(0x0004), - field_0008: buf.readUInt32LE(0x0008), - field_000C: buf.readUInt8(0x000c), - }; -} diff --git a/src/idz/encoder/_team.ts b/src/idz/encoder/_team.ts index 4c8c59b..18543d2 100644 --- a/src/idz/encoder/_team.ts +++ b/src/idz/encoder/_team.ts @@ -1,13 +1,13 @@ import iconv = require("iconv-lite"); -import { JoinAutoTeamResponse } from "../response/joinAutoTeam"; +import { CreateAutoTeamResponse } from "../response/createAutoTeam"; import { LoadTeamResponse } from "../response/loadTeam"; import { encodeChara } from "./_chara"; -export function _team(res: JoinAutoTeamResponse | LoadTeamResponse) { +export function _team(res: CreateAutoTeamResponse | LoadTeamResponse) { const buf = Buffer.alloc(0x0ca0); - if (res.type === "join_auto_team_res") { + if (res.type === "create_auto_team_res") { buf.writeInt16LE(0x007c, 0x0000); } else { buf.writeInt16LE(0x0078, 0x0000); diff --git a/src/idz/encoder/index.ts b/src/idz/encoder/index.ts index ebdcc99..0cfaa7f 100644 --- a/src/idz/encoder/index.ts +++ b/src/idz/encoder/index.ts @@ -36,15 +36,15 @@ function encode(res: Response): Buffer { case "check_team_name_res": return checkTeamName(res); + case "create_auto_team_res": + return _team(res); + case "create_team_res": return createTeam(res); case "discover_profile_res": return discoverProfile(res); - case "join_auto_team_res": - return _team(res); - case "generic_res": return generic(res); diff --git a/src/idz/encoder/loadProfile2.ts b/src/idz/encoder/loadProfile2.ts index 230af87..066b14f 100644 --- a/src/idz/encoder/loadProfile2.ts +++ b/src/idz/encoder/loadProfile2.ts @@ -75,7 +75,7 @@ export function loadProfile2(res: LoadProfileResponse2) { buf.writeUInt16LE(res.unlocks.gauges, 0x00b8); buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x01e8); buf.writeUInt16LE(res.unlocks.music, 0x01ec); - buf.writeUInt16LE(0, 0x037c); // Team leader + buf.writeUInt16LE(res.teamLeader ? 1 : 0, 0x037c); encodeMission(res.missions.team).copy(buf, 0x038a); buf.writeUInt16LE(0xffff, 0x0388); // [1] buf.writeUInt32LE(res.aimeId, 0x03b8); diff --git a/src/idz/encoder/loadProfile3.ts b/src/idz/encoder/loadProfile3.ts index e88f161..abdb551 100644 --- a/src/idz/encoder/loadProfile3.ts +++ b/src/idz/encoder/loadProfile3.ts @@ -75,7 +75,7 @@ export function loadProfile3(res: LoadProfileResponse3) { buf.writeUInt16LE(res.unlocks.gauges, 0x00b8); buf.writeUInt32LE(res.unlocks.lastMileageReward, 0x0218); buf.writeUInt16LE(res.unlocks.music, 0x021c); - buf.writeUInt16LE(0, 0x0456); // Team leader + buf.writeUInt16LE(res.teamLeader ? 1 : 0, 0x0456); // Team leader encodeMission(res.missions.team).copy(buf, 0x0460); buf.writeUInt16LE(0xffff, 0x0462); // [1] buf.writeUInt32LE(res.aimeId, 0x0494); diff --git a/src/idz/handler/_team.ts b/src/idz/handler/_team.ts index 9e5797d..57fdea4 100644 --- a/src/idz/handler/_team.ts +++ b/src/idz/handler/_team.ts @@ -1,34 +1,33 @@ -import { ExtId } from "../model/base"; import { Team } from "../model/team"; -import { JoinAutoTeamRequest } from "../request/joinAutoTeam"; -import { LoadTeamRequest } from "../request/loadTeam"; -import { JoinAutoTeamResponse } from "../response/joinAutoTeam"; -import { LoadTeamResponse } from "../response/loadTeam"; import { Repositories } from "../repo"; +import { Id } from "../../db"; -// Even if a profile does not belong to a team, a team must still be loaded -// (and then ignored by the client). +// Bleh. This factorization is kind of messy. -const dummy: Team = { - extId: 0 as ExtId, - name: "", - nameBg: 0, - nameFx: 0, - registerTime: new Date(0), -}; - -export function _team( +export async function _fixupPrevTeam( w: Repositories, - req: JoinAutoTeamRequest | LoadTeamRequest -): JoinAutoTeamResponse | LoadTeamResponse { - const bits = { - team: dummy, - members: [], - }; + prevTeamId: Id | undefined +): Promise { + if (prevTeamId === undefined) { + return; + } - if (req.type === "join_auto_team_req") { - return { type: "join_auto_team_res", ...bits }; - } else { - return { type: "load_team_res", ...bits }; + const remaining = await w.teamMembers().loadRoster(prevTeamId); + + if (remaining.length === 0) { + // Last member left, GC previous team + + await w.teams().delete(prevTeamId); + } else if (remaining.find(member => member.leader) === undefined) { + // Leader left, appoint new leader by seniority + + remaining.sort((x, y) => x.joinTime.getTime() - y.joinTime.getTime()); + + // (need to look up new leader's db id from aime id. ick) + + const newLeader = remaining[remaining.length - 1]; + const newLeaderId = await w.profile().find(newLeader.profile.aimeId); + + await w.teamMembers().makeLeader(prevTeamId, newLeaderId); } } diff --git a/src/idz/handler/createAutoTeam.ts b/src/idz/handler/createAutoTeam.ts new file mode 100644 index 0000000..b6636db --- /dev/null +++ b/src/idz/handler/createAutoTeam.ts @@ -0,0 +1,121 @@ +import { TeamAuto } from "../model/team"; +import { Repositories } from "../repo"; +import { CreateAutoTeamRequest } from "../request/createAutoTeam"; +import { CreateAutoTeamResponse } from "../response/createAutoTeam"; + +interface AutoTeamTemplate { + prefix: string; + nameBg: number; +} + +// Hard-code these for the time being, since AFAIK there are only three. +// These are all references to teams in the Initial D manga/anime. +// If any more auto-teams are added/discovered they *MUST* be added to the +// *END* of this list. Otherwise duplicate auto-teams will be created. + +const autoTeams: AutoTeamTemplate[] = [ + { + // "Speed Stars" + prefix: "スピードスターズ", + nameBg: 0, + }, + { + // "Red Suns" + prefix: "レッドサンズ", + nameBg: 1, + }, + { + // "Night Kids" (even though it's written like "Night Keys"...) + prefix: "ナイトキッズ", + nameBg: 2, + }, +]; + +function incrementAuto(prev: TeamAuto): TeamAuto { + if (prev.nameIdx < autoTeams.length - 1) { + return { nameIdx: prev.nameIdx + 1, serialNo: prev.serialNo }; + } else { + return { nameIdx: 0, serialNo: prev.serialNo + 1 }; + } +} + +export async function createAutoTeam( + w: Repositories, + req: CreateAutoTeamRequest +): Promise { + const now = new Date(); + const { aimeId } = req; + + const peek = await w.teamAuto().peek(); + let nextAuto: TeamAuto; + + // + // Determine if we need to create a new team or not + // + + if (peek !== undefined) { + // Look at the highest-numbered auto team. Is it full? + + const [lastAuto, lastTeamId] = peek; + const occupancy = await w.teamReservations().occupancyHack(lastTeamId); + + console.log(occupancy); + + if (occupancy < 6) { + // Team isn't full, so return this one + await w.teamReservations().reserveHack(lastTeamId, aimeId, now); + + return { + type: "create_auto_team_res", + team: await w.teams().load(lastTeamId), + members: await w.teamMembers().loadRoster(lastTeamId), + }; + } + + // Team full, need to create a new one + + nextAuto = incrementAuto(lastAuto); + } else { + // No teams exist at all, seed the system with SpeedStars001. + + nextAuto = { serialNo: 1, nameIdx: 0 }; + } + + // + // Build the new team + // + + // Make a three-digit serial number using full-width digits + + let { serialNo, nameIdx } = nextAuto; + let name = ""; + + for (let i = 0; i < 3; i++) { + name = String.fromCodePoint(0xff10 + (serialNo % 10)) + name; + serialNo = (serialNo / 10) | 0; + } + + // Prepend the name prefix + + name = autoTeams[nameIdx].prefix + name; + + // Register the new team, make the requestor its leader + + const spec = { + name, + nameBg: autoTeams[nameIdx].nameBg, + nameFx: 0, + registerTime: now, + }; + + const [newTeamId, newTeamExtId] = await w.teams().create(spec); + + await w.teamAuto().push(newTeamId, nextAuto); + await w.teamReservations().reserveHack(newTeamId, aimeId, now, "leader"); + + return { + type: "create_auto_team_res", + team: { ...spec, extId: newTeamExtId }, + members: [], + }; +} diff --git a/src/idz/handler/createProfile.ts b/src/idz/handler/createProfile.ts index 725e08f..85fb47a 100644 --- a/src/idz/handler/createProfile.ts +++ b/src/idz/handler/createProfile.ts @@ -47,6 +47,8 @@ export async function createProfile( await w.unlocks().save(profileId, unlocks); await w.tickets().save(profileId, {}); + await w.teamReservations().commitHack(aimeId); + return { type: "generic_res", status: aimeId, // "Generic response" my fucking *ass* diff --git a/src/idz/handler/createTeam.ts b/src/idz/handler/createTeam.ts index 1c19b3d..5d41f4d 100644 --- a/src/idz/handler/createTeam.ts +++ b/src/idz/handler/createTeam.ts @@ -1,16 +1,58 @@ -import { ExtId } from "../model/base"; -import { Team } from "../model/team"; +import { _fixupPrevTeam } from "./_team"; import { CreateTeamRequest } from "../request/createTeam"; import { CreateTeamResponse } from "../response/createTeam"; import { Repositories } from "../repo"; -export function createTeam( +export async function createTeam( w: Repositories, req: CreateTeamRequest -): CreateTeamResponse { +): Promise { + const profileId = await w.profile().find(req.aimeId); + const prevTeamId = await w.teamMembers().findTeam(profileId); + const now = new Date(); + + // Create the new team... + + const teamSpec = { + name: req.teamName, + nameBg: req.nameBg, + nameFx: 0, + registerTime: now, + }; + + const [teamId, teamExtId] = await w.teams().create(teamSpec); + + await w.teamMembers().join(teamId, profileId, now); + await w.teamMembers().makeLeader(teamId, profileId); + await _fixupPrevTeam(w, prevTeamId); + + // Fix up previous team. The previous team's extid is explicitly sent in the + // request, but why rely on it if you don't have to? + + if (prevTeamId !== undefined) { + const remaining = await w.teamMembers().loadRoster(prevTeamId); + + if (remaining.length === 0) { + // Last member left, GC previous team + + await w.teams().delete(prevTeamId); + } else if (remaining.find(member => member.leader) === undefined) { + // Leader left, appoint new leader by seniority + + remaining.sort((x, y) => x.joinTime.getTime() - y.joinTime.getTime()); + + // (need to look up new leader's db id from aime id. ick) + + const newLeader = remaining[remaining.length - 1]; + const newLeaderId = await w.profile().find(newLeader.profile.aimeId); + + await w.teamMembers().makeLeader(prevTeamId, newLeaderId); + } + } + return { type: "create_team_res", status: 0, - teamExtId: 3 as ExtId, + teamExtId, }; } diff --git a/src/idz/handler/index.ts b/src/idz/handler/index.ts index 2ad4991..717ff8b 100644 --- a/src/idz/handler/index.ts +++ b/src/idz/handler/index.ts @@ -1,5 +1,5 @@ -import { _team } from "./_team"; import { checkTeamName } from "./checkTeamName"; +import { createAutoTeam } from "./createAutoTeam"; import { createProfile } from "./createProfile"; import { createTeam } from "./createTeam"; import { discoverProfile } from "./discoverProfile"; @@ -15,6 +15,7 @@ import { loadProfile } from "./loadProfile"; import { loadReward as loadRewardTable } from "./loadRewardTable"; import { loadServerList } from "./loadServerList"; import { loadStocker } from "./loadStocker"; +import { loadTeam } from "./loadTeam"; import { loadTeamRanking } from "./loadTeamRanking"; import { loadTopTen } from "./loadTopTen"; import { lockGarage } from "./lockGarage"; @@ -48,15 +49,15 @@ export async function dispatch( case "check_team_name_req": return checkTeamName(w, req); + case "create_auto_team_req": + return createAutoTeam(w, req); + case "create_profile_req": return createProfile(w, req); case "create_team_req": return createTeam(w, req); - case "join_auto_team_req": - return _team(w, req); - case "load_2on2_req": return load2on2(w, req); @@ -97,7 +98,7 @@ export async function dispatch( return loadStocker(w, req); case "load_team_req": - return _team(w, req); + return loadTeam(w, req); case "load_top_ten_req": return loadTopTen(w, req); diff --git a/src/idz/handler/loadProfile.ts b/src/idz/handler/loadProfile.ts index 3bc9aa6..20051e7 100644 --- a/src/idz/handler/loadProfile.ts +++ b/src/idz/handler/loadProfile.ts @@ -9,6 +9,8 @@ export async function loadProfile( const { aimeId } = req; const profileId = await w.profile().find(aimeId); + const teamId = await w.teamMembers().findTeam(profileId); + const leaderId = teamId && (await w.teamMembers().findLeader(teamId)); // Promise.all would be messy here, who cares anyway this isn't supposed to // be a high-performance server. @@ -25,6 +27,7 @@ export async function loadProfile( const timeAttack = await w.timeAttack().loadAll(profileId); const unlocks = await w.unlocks().load(profileId); const tickets = await w.tickets().load(profileId); + const team = teamId && (await w.teams().load(teamId)); return { type: "load_profile_res", @@ -36,7 +39,8 @@ export async function loadProfile( fame: profile.fame, dpoint: profile.dpoint, mileage: profile.mileage, - // teamId: TODO + teamId: team && team.extId, + teamLeader: profileId === leaderId, settings, chara, titles, diff --git a/src/idz/handler/loadTeam.ts b/src/idz/handler/loadTeam.ts new file mode 100644 index 0000000..16ed97c --- /dev/null +++ b/src/idz/handler/loadTeam.ts @@ -0,0 +1,37 @@ +import { ExtId } from "../model/base"; +import { Team } from "../model/team"; +import { LoadTeamRequest } from "../request/loadTeam"; +import { LoadTeamResponse } from "../response/loadTeam"; +import { Repositories } from "../repo"; + +// Even if a profile does not belong to a team, a team must still be loaded +// (and then ignored by the client). + +const dummyResp: LoadTeamResponse = { + type: "load_team_res", + team: { + extId: 0 as ExtId, + name: "", + nameBg: 0, + nameFx: 0, + registerTime: new Date(0), + }, + members: [], +}; + +export async function loadTeam( + w: Repositories, + req: LoadTeamRequest +): Promise { + if (req.teamExtId === undefined) { + return dummyResp; + } + + const teamId = await w.teams().find(req.teamExtId); + + return { + type: "load_team_res", + team: await w.teams().load(teamId), + members: await w.teamMembers().loadRoster(teamId), + }; +} diff --git a/src/idz/request/createAutoTeam.ts b/src/idz/request/createAutoTeam.ts new file mode 100644 index 0000000..d77d0cb --- /dev/null +++ b/src/idz/request/createAutoTeam.ts @@ -0,0 +1,8 @@ +import { AimeId } from "../../model"; + +export interface CreateAutoTeamRequest { + type: "create_auto_team_req"; + aimeId: AimeId; + field_0008: number; + field_000C: number; +} diff --git a/src/idz/request/index.ts b/src/idz/request/index.ts index d9e7aaf..923bfc1 100644 --- a/src/idz/request/index.ts +++ b/src/idz/request/index.ts @@ -2,7 +2,7 @@ import { CheckTeamNameRequest } from "./checkTeamName"; import { CreateProfileRequest } from "./createProfile"; import { CreateTeamRequest } from "./createTeam"; import { DiscoverProfileRequest } from "./discoverProfile"; -import { JoinAutoTeamRequest } from "./joinAutoTeam"; +import { CreateAutoTeamRequest } from "./createAutoTeam"; import { Load2on2Request } from "./load2on2"; import { LoadConfigRequest } from "./loadConfig"; import { LoadConfigRequest2 } from "./loadConfig2"; @@ -40,10 +40,10 @@ import { UpdateUserLogRequest } from "./updateUserLog"; export type Request = | CheckTeamNameRequest + | CreateAutoTeamRequest | CreateProfileRequest | CreateTeamRequest | DiscoverProfileRequest - | JoinAutoTeamRequest | Load2on2Request | LoadConfigRequest | LoadConfigRequest2 diff --git a/src/idz/request/joinAutoTeam.ts b/src/idz/request/joinAutoTeam.ts deleted file mode 100644 index 99423cf..0000000 --- a/src/idz/request/joinAutoTeam.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface JoinAutoTeamRequest { - type: "join_auto_team_req"; - field_0004: number; - field_0008: number; - field_000C: number; -} diff --git a/src/idz/response/createAutoTeam.ts b/src/idz/response/createAutoTeam.ts new file mode 100644 index 0000000..70237f7 --- /dev/null +++ b/src/idz/response/createAutoTeam.ts @@ -0,0 +1,5 @@ +import { BaseTeamResponse } from "./_team"; + +export interface CreateAutoTeamResponse extends BaseTeamResponse { + type: "create_auto_team_res"; +} diff --git a/src/idz/response/index.ts b/src/idz/response/index.ts index dd19dec..ba65c36 100644 --- a/src/idz/response/index.ts +++ b/src/idz/response/index.ts @@ -1,8 +1,8 @@ import { CheckTeamNameResponse } from "./checkTeamName"; +import { CreateAutoTeamResponse } from "./createAutoTeam"; import { CreateTeamResponse } from "./createTeam"; import { DiscoverProfileResponse } from "./discoverProfile"; import { GenericResponse } from "./generic"; -import { JoinAutoTeamResponse } from "./joinAutoTeam"; import { Load2on2Response } from "./load2on2"; import { LoadConfigResponse } from "./loadConfig"; import { LoadConfigResponse2 } from "./loadConfig2"; @@ -31,10 +31,10 @@ import { UnlockProfileResponse } from "./unlockProfile"; export type Response = | CheckTeamNameResponse + | CreateAutoTeamResponse | CreateTeamResponse | DiscoverProfileResponse | GenericResponse - | JoinAutoTeamResponse | Load2on2Response | LoadConfigResponse | LoadConfigResponse2 diff --git a/src/idz/response/joinAutoTeam.ts b/src/idz/response/joinAutoTeam.ts deleted file mode 100644 index fe87d0d..0000000 --- a/src/idz/response/joinAutoTeam.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseTeamResponse } from "./_team"; - -export interface JoinAutoTeamResponse extends BaseTeamResponse { - type: "join_auto_team_res"; -} diff --git a/src/idz/response/loadProfile.ts b/src/idz/response/loadProfile.ts index c7596ae..2e41537 100644 --- a/src/idz/response/loadProfile.ts +++ b/src/idz/response/loadProfile.ts @@ -19,6 +19,7 @@ interface LoadProfileResponseBase { dpoint: number; mileage: number; teamId?: number; + teamLeader: boolean; settings: Settings; chara: Chara; titles: Set;