kozukata-toa/src/servers/aimedb/handlers/felica-conversion.ts

292 lines
8.1 KiB
TypeScript

// On real ALL.Net, there's just a massive database of every supported FeliCa's
// IDm -> access code mappings. Since we don't have access to such information,
// every FeliCa card will be treated like FeliCa mobile: an access code is
// generated, and then stored with its IDm for future lookups.
//
// This also means you'll need to get some more keys, but if you're lazy
// you can probably just set
// - RESERVED_CARD_PREFIX to something not starting with 0 or 3
// - RESERVED_CARD_KEY to a random 16-digit hex string, where each digit displays
// exactly once.
import {
FelicaExtendedLookupRequestStruct,
FelicaExtendedLookupResponseStruct,
FelicaLookupRequestStruct,
FelicaLookupResponseStruct,
} from "../types/felica-conversion";
import { PacketHeaderStruct } from "../types/header";
import { CalculateAccessCode } from "../utils/access-code";
import { IsSupportedFelicaMobile, IsSupportedFelica } from "../utils/felica";
import { AimeCard, FelicaCardLookup, FelicaMobileLookup } from "external/db/entity/aimedb";
import { CommandId, CompanyCodes, PortalRegistration, ResultCodes } from "lib/constants/aimedb";
import { DATA_FORMAT_CODE_MAP } from "lib/constants/felica";
import CreateLogCtx from "lib/logger/logger";
import { Config } from "lib/setup/config";
import type { AimeDBHandlerFn } from "../types/handlers";
import type { ValidFeliCaIcTypes } from "../utils/felica";
import type { FelicaBaseLookup } from "external/db/entity/aimedb";
const logger = CreateLogCtx(__filename);
async function generateAndInsertNewAccessCode<T extends typeof FelicaBaseLookup>(
table: T,
cardType: "FeliCa Card" | "FeliCa Mobile",
idm: bigint,
prefix: string,
key: string
): Promise<FelicaBaseLookup | null> {
const mostRecentRow = await table.findOne({
order: {
id: "desc",
},
});
const nextId = mostRecentRow ? mostRecentRow.id + 1 : 1;
const accessCode = CalculateAccessCode(nextId, prefix, key);
logger.debug(`Created ${cardType} access code for serial ${nextId}.`, {
nextId,
accessCode,
});
const card = table.construct({ idm: idm.toString(16), accessCode });
try {
await card.save();
} catch (err) {
logger.error("Failed to insert new lookup entry into the database.", { err });
return null;
}
return card;
}
async function generateNewFeliCa(idm: bigint, icType: ValidFeliCaIcTypes) {
if (!IsSupportedFelica(icType)) {
throw new Error("Invalid IC type.");
}
if (IsSupportedFelicaMobile(icType)) {
if (!Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY) {
logger.error(
"AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY is not set in config file. Cannot generate a new access code.",
{ idm, icType }
);
return null;
}
return generateAndInsertNewAccessCode(
FelicaMobileLookup,
"FeliCa Mobile",
idm,
"01035",
Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY
);
}
if (!Config.AIMEDB_CONFIG.RESERVED_CARD_KEY || !Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX) {
logger.error(
"AIMEDB_CONFIG.RESERVED_CARD_KEY or AIMEDB_CONFIG.RESERVED_CARD_PREFIX is not set in config file. Cannot generate a new access code.",
{ idm, icType }
);
return null;
}
return generateAndInsertNewAccessCode(
FelicaCardLookup,
"FeliCa Card",
idm,
Config.AIMEDB_CONFIG.RESERVED_CARD_PREFIX,
Config.AIMEDB_CONFIG.RESERVED_CARD_KEY
);
}
// This is supposed to be just a lookup handler, but my guess is that most games assume that there
// is already a valid card in the database if the response is successful and don't bother to try
// registering a new card, if the card is not FeliCa Mobile, since the game will happily display
// access code 00000000000000000000.
export const FelicaLookupHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = async (
header,
data
) => {
header.commandId = CommandId.FELICA_LOOKUP_RESPONSE;
header.length = PacketHeaderStruct.baseSize + FelicaLookupResponseStruct.baseSize;
header.result = ResultCodes.SUCCESS;
const req = new FelicaLookupRequestStruct(data);
const resp = new FelicaLookupResponseStruct();
if (!IsSupportedFelica(req.icType)) {
header.result = ResultCodes.INVALID_AIME_ID;
resp.felicaIndex = -1;
return resp;
}
const table = IsSupportedFelicaMobile(req.icType) ? FelicaMobileLookup : FelicaCardLookup;
let result: FelicaBaseLookup | null = await table.findOne({
where: {
idm: req.idm.toString(16),
},
});
if (!result) {
const tmp = await generateNewFeliCa(req.idm, req.icType);
if (!tmp) {
header.result = ResultCodes.UNKNOWN_ERROR;
return resp;
}
result = tmp;
}
header.result = ResultCodes.SUCCESS;
resp.felicaIndex = result.id;
resp.accessCode.set(Buffer.from(result.accessCode, "hex"));
return resp;
};
// This is supposed to be just a lookup handler, but my guess is that most games assume that there
// is already a valid card in the database if the response is successful and don't bother to try
// registering a new card, if the card is not FeliCa Mobile, since the game will happily display
// access code 00000000000000000000.
export const FelicaExtendedLookupHandler: AimeDBHandlerFn<"FelicaExtendedLookupResponse"> = async (
header,
data
) => {
header.commandId = CommandId.EXTENDED_FELICA_ACCOUNT_RESPONSE;
header.length = PacketHeaderStruct.baseSize + FelicaExtendedLookupResponseStruct.baseSize;
header.result = ResultCodes.SUCCESS;
const req = new FelicaExtendedLookupRequestStruct(data);
const resp = new FelicaExtendedLookupResponseStruct();
resp.accountId = -1;
logger.debug("Parsed response body.", { req });
if (req.companyCode < 0 || req.companyCode > 4) {
header.result = ResultCodes.INVALID_AIME_ID;
return resp;
}
if (!IsSupportedFelica(req.icType)) {
header.result = ResultCodes.INVALID_AIME_ID;
return resp;
}
const table = IsSupportedFelicaMobile(req.icType) ? FelicaMobileLookup : FelicaCardLookup;
const result: FelicaBaseLookup | null = await table.findOne({
where: {
idm: req.idm.toString(16),
},
});
let accessCode;
if (result) {
accessCode = result.accessCode;
} else {
const row = await generateNewFeliCa(req.idm, req.icType);
if (!row) {
header.result = ResultCodes.UNKNOWN_ERROR;
return resp;
}
accessCode = row.accessCode;
}
// HACK: What does official AimeDB do if the DFC is not valid?
resp.companyCode = DATA_FORMAT_CODE_MAP[req.dataFormatCode] ?? CompanyCodes.SEGA;
resp.accessCode.set(Buffer.from(accessCode, "hex"));
const maybeCard = await AimeCard.findOne({
where: { accessCode },
});
if (maybeCard) {
// TODO: Actually handle portal state when we get a webUI
resp.portalRegistered = PortalRegistration.UNREGISTERED;
resp.accountId = maybeCard.id;
}
return resp;
};
export const FelicaRegisterHandler: AimeDBHandlerFn<"FelicaLookupResponse"> = async (
header,
data
) => {
header.commandId = CommandId.FELICA_LOOKUP_RESPONSE;
header.length = PacketHeaderStruct.baseSize + FelicaLookupResponseStruct.baseSize;
const req = new FelicaLookupRequestStruct(data);
const resp = new FelicaLookupResponseStruct();
if (!IsSupportedFelicaMobile(req.icType)) {
logger.error("Rejecting card because it is not FeliCa Mobile.", {
idm: req.idm,
romType: req.romType,
icType: req.icType,
timing: req.timing,
});
header.result = ResultCodes.INVALID_AIME_ID;
resp.felicaIndex = -1;
return resp;
}
const result = await FelicaMobileLookup.findOne({
where: {
idm: req.idm.toString(16),
},
});
if (result) {
header.result = ResultCodes.ID_ALREADY_REGISTERED;
resp.felicaIndex = result.id;
resp.accessCode.set(Buffer.from(result.accessCode, "hex"));
return resp;
}
if (!Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY) {
logger.error(
"AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY is not set in config file. Cannot register FeliCa Mobile ID.",
{ req }
);
header.result = ResultCodes.UNKNOWN_ERROR;
resp.felicaIndex = -1;
return resp;
}
const row = await generateAndInsertNewAccessCode(
FelicaMobileLookup,
"FeliCa Mobile",
req.idm,
"01035",
Config.AIMEDB_CONFIG.AIME_MOBILE_CARD_KEY
);
if (!row) {
header.result = ResultCodes.UNKNOWN_ERROR;
resp.felicaIndex = -1;
return resp;
}
header.result = ResultCodes.SUCCESS;
resp.felicaIndex = row.id;
resp.accessCode.set(Buffer.from(row.accessCode, "hex"));
return resp;
};