leonitousconforti/tinyburg

View on GitHub
packages/insight/src/agents/get-mission-data.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import "frida-il2cpp-bridge";

import type { IMissionAgentExports } from "../shared/mission-agent-exports.js";

import { readField } from "../helpers/read.js";
import { TinyTowerFridaAgent } from "./base-frida-agent.js";
import { readEnumFields } from "../helpers/get-enum-fields.js";
import { copyDictionaryToJs } from "../helpers/copy-dictionary-to-js.js";

export class GetMissionData extends TinyTowerFridaAgent<GetMissionData> {
    public loadDependencies() {
        const csharpAssembly = Il2Cpp.domain.assembly("Assembly-CSharp");
        const AppUtilClass = csharpAssembly.image.class("AppUtil");
        const VMissionClass = csharpAssembly.image.class("VMission");
        const MissionTypeClass = csharpAssembly.image.class("MissionType");
        const VMissionDataClass = csharpAssembly.image.class("VMissionData");
        const missionsDictionary = VMissionDataClass.field<Il2Cpp.Object>("missions").value;
        const tipMissionsDictionary = VMissionDataClass.field<Il2Cpp.Object>("tipMissions").value;
        const tutorialMissionsDictionary = VMissionDataClass.field<Il2Cpp.Object>("tutMissions").value;

        return {
            AppUtilClass,
            VMissionClass,
            MissionTypeClass,
            VMissionDataClass: {
                dependency: VMissionDataClass,
                meta: { callStaticConstructor: true },
            },
            missionsDictionary,
            tipMissionsDictionary,
            tutorialMissionsDictionary,
        };
    }

    public retrieveData() {
        // Extract the game version
        const version = this.dependencies.AppUtilClass.method<Il2Cpp.String>("VersionString").invoke().content;

        // Extract the MissionType enum fields
        const missionTypeEnumFields = readEnumFields(this.dependencies.MissionTypeClass);

        const extractMissionEntries = (object: Il2Cpp.Object) =>
            // Copy the missions dictionary over to JS and map the entries (key is the tutorial
            // mission index and value is the mission data) to a list of entries with the mission's properties
            Object.entries(copyDictionaryToJs<number, Il2Cpp.Object>(object))
                .map(
                    ([index, data]) =>
                        [
                            index,
                            ["id", "mType", "text", "charId", "floorId", "floorType", "count"].map((property) => [
                                property,
                                readField(data.field(property)),
                            ]),
                        ] as const
                )

                // Reassemble the list of mission entries into an object
                .map(([index, data]) => [index, Object.fromEntries(data)] as const);

        // Extract the missions
        const tutorialMissionEntries = extractMissionEntries(this.dependencies.tutorialMissionsDictionary);
        const tipMIssionsEntries = extractMissionEntries(this.dependencies.tipMissionsDictionary);
        const missionEntries = extractMissionEntries(this.dependencies.missionsDictionary);

        return {
            TTVersion: version || "unknown",
            missionTypeEnumFields,
            missions: Object.fromEntries(missionEntries),
            tipMissions: Object.fromEntries(tipMIssionsEntries),
            tutorialMissions: Object.fromEntries(tutorialMissionEntries),
        };
    }

    public transformToSourceCode() {
        // Import for the floor type enum
        // eslint-disable-next-line quotes
        const floorTypeEnumImport = 'import { FloorType } from "./floors.js";\n';

        // Source code for the mission type enum
        const missionTypeEnumFieldsSource = this.transformEnumFieldsToSource(this.data.missionTypeEnumFields);
        const missionTypeSourceTS = `export enum MissionType {${missionTypeEnumFieldsSource}}\n`;

        // eslint-disable-next-line unicorn/consistent-function-scoping
        const formatMissionSource = (name: string, data: { [k: string]: unknown }) =>
            `export const ${name} = ${JSON.stringify(data)} as const;`
                .replaceAll(/"floorType":\s*"(\w+)"/gm, "floorType: FloorType.$1")
                .replaceAll(/"mType":\s*"(\w+)"/gm, "missionType: MissionType.$1");

        // Source code for the missions
        const tutorialMissionsSourceTs = formatMissionSource("tutorialMissions", this.data.tutorialMissions);
        const tipMissionsSourceTs = formatMissionSource("tipMissions", this.data.tipMissions);
        const missionsSourceTs = formatMissionSource("missions", this.data.missions);
        const tutorialMissionSourceTs = "export type TutorialMission = typeof tutorialMissions;\n";
        const tipMissionSourceTs = "export type TipMission = typeof tipMissions;\n";
        const missionSourceTs = "export type Mission = typeof missions;\n";

        return (
            `// TinyTower version: ${this.data.TTVersion}\n` +
            floorTypeEnumImport +
            "\n" +
            missionTypeSourceTS +
            "\n" +
            tutorialMissionsSourceTs +
            tutorialMissionSourceTs +
            "\n" +
            tipMissionsSourceTs +
            tipMissionSourceTs +
            "\n" +
            missionsSourceTs +
            missionSourceTs
        );
    }
}

// Main entry point exported for when this file is compiled as a frida agent.
const rpcExports: IMissionAgentExports = {
    main: async () => {
        const instance = await new GetMissionData().start();
        return instance.transformToSourceCode();
    },
};
rpc.exports = rpcExports as unknown as RpcExports;