292 lines
8.1 KiB
TypeScript
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;
|
|
};
|