XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
UTF-8
import fs from "fs";
import path from "path";
import { repoDir } from "../helpers/pathHelper.ts";
import { args } from "../helpers/commandLineArguments.ts";
import type { Request } from "express";
import { version_compare } from "../helpers/inventoryHelpers.ts";
import configDefaults from "../../config-vanilla.json" with { type: "json" };

export type TRegionId = "ASIA" | "OCEANIA" | "EUROPE" | "RUSSIA" | "NORTH_AMERICA" | "SOUTH_AMERICA";

export interface IHubServer {
    address: string;
    regions?: TRegionId[];
    dtlsUnsupported?: boolean;
}

export interface IWebuiConfig {
    enabled?: boolean;
    adminOnly?: boolean;
    defaultLanguage?: string;
    nonAdminPermissions?: Record<string, boolean | Record<string, boolean>>;
}

export type TLogLevel = "error" | "warn" | "info" | "http" | "debug" | "trace";

type TQolConfigKey =
    | "tutorialGivesStanceMods"
    | "twentythreeHourMasteryRankCooldown"
    | "doubleDailySynthesisEndoReward"
    | "tylRegorDropsTwoEquinoxParts"
    | "railjackExtraResourceBundlesReward"
    | "railjackSteelEssencesReward";

export interface IConfig {
    /** @deprecated */ mongodbUrl?: string;
    database:
        | string
        | {
              engine: "MongoDB 7.0" | "MongoDB 8.0";
              dbPath: string;
          };
    logger: {
        files: boolean;
        /** @deprecated */ level?: TLogLevel;
        fileLevel?: TLogLevel;
        consoleLevel?: TLogLevel;
        format?: string;
    };
    myAddress: string;
    bindAddress?: string;
    httpPort?: number;
    httpsPort?: number;
    httpsCertFile?: string;
    httpsKeyFile?: string;
    ircExecutable?: string;
    ircAddress?: string;
    hubExecutable?: string;
    /** @deprecated */ hubAddress?: string;
    hubServers?: IHubServer[];
    noHubDiscrimination?: boolean;
    /** @deprecated */ nrsAddress?: string;
    nrsAddresses?: string[];
    dtls?: number;
    administratorNames?: string[];
    autoCreateAccount?: boolean;
    fallbackBuildLabel?: string;
    skipTutorial?: boolean;
    fullyStockedVendors?: boolean;
    skipClanKeyCrafting?: boolean;
    webui?: IWebuiConfig;
    unfaithfulBugFixes?: {
        ignore1999LastRegionPlayed?: boolean;
        fixXtraCheeseTimer?: boolean;
        useAnniversaryTagForOldGoals?: boolean;
        giveBreedingGroundsRewardsAtSum?: boolean;
    };
    worldState?: {
        creditBoostMultiplier?: number;
        affinityBoostMultiplier?: number;
        resourceBoostMultiplier?: number;
        tennoLiveRelay?: boolean;
        baroTennoConRelay?: boolean;
        baroAlwaysAvailable?: boolean;
        baroFullyStocked?: boolean;
        baroRelayOverride?: number;
        evilBaroStage?: number;
        varziaFullyStocked?: boolean;
        vanguardVaultRelics?: boolean;
        wolfHunt?: number;
        scarletSpear?: boolean;
        orphixVenom?: boolean;
        bloodOfPerita?: boolean;
        longShadow?: boolean;
        hallowedFlame?: boolean;
        anniversary?: number;
        hallowedNightmares?: boolean;
        hallowedNightmaresRewardsOverride?: number;
        naberusNightsOverride?: boolean;
        proxyRebellion?: boolean;
        proxyRebellionRewardsOverride?: number;
        voidCorruption2025Week1?: boolean;
        voidCorruption2025Week2?: boolean;
        voidCorruption2025Week3?: boolean;
        voidCorruption2025Week4?: boolean;
        dagathAlerts2026Week1?: boolean;
        dagathAlerts2026Week2?: boolean;
        dagathAlerts2026Week3?: boolean;
        dagathAlerts2026Week4?: boolean;
        starDaysAlerts2026Week1?: boolean;
        starDaysAlerts2026Week2?: boolean;
        starDaysAlerts2026Week3?: boolean;
        starDaysAlerts2026Week4?: boolean;
        qtccAlerts?: boolean;
        destiny2TributeAlert?: boolean;
        galleonOfGhouls?: number;
        ghoulEmergenceOverride?: boolean;
        plagueStarOverride?: boolean;
        starDaysOverride?: boolean;
        saintPatrickOverride?: boolean;
        prideOverride?: boolean;
        xmasOverride?: boolean;
        lunarNewYear?: string;
        dogDaysOverride?: boolean;
        dogDaysRewardsOverride?: number;
        operationAtramentum?: boolean;
        operationAtramentumProgressOverride?: number;
        bellyOfTheBeast?: boolean;
        bellyOfTheBeastProgressOverride?: number;
        eightClaw?: boolean;
        eightClawProgressOverride?: number;
        thermiaFracturesOverride?: boolean;
        thermiaFracturesProgressOverride?: number;
        eidolonOverride?: string;
        vallisOverride?: string;
        duviriOverride?: string;
        nightwaveOverride?: string;
        nightwaveEpisode?: number;
        classicAlerts?: boolean;
        allTheFissures?: string;
        varziaOverride?: string;
        circuitGameModes?: string[];
        darvoStockMultiplier?: number;
        communitySynthesisTarget?: number;
        communitySynthesisProgress?: number;
        snowdayShowdown?: boolean;
        breedingGrounds?: boolean;
    };
    serversideQualityOfLife?: Record<TQolConfigKey, boolean | null>;
    inventoryDefaults?: Record<string, any>;
    tunables?: {
        useLoginToken?: boolean;
        prohibitSkipMissionStartTimer?: boolean;
        prohibitDisableProfanityFilter?: boolean;
        prohibitFovOverride?: boolean;
        prohibitFreecam?: boolean;
        prohibitTeleport?: boolean;
        prohibitScripts?: boolean;
        motd?: string;
        udpProxyUpstream?: string;
    };
    dev?: {
        keepVendorsExpired?: boolean;
    };
}

export const configRemovedOptionsKeys = [
    "unlockallShipFeatures",
    "testQuestKey",
    "lockTime",
    "starDays",
    "platformCDNs",
    "completeAllQuests",
    "worldSeed",
    "unlockAllQuests",
    "unlockAllMissions",
    "version",
    "matchmakingBuildId",
    "buildLabel",
    "infiniteResources",
    "testMission",
    "skipStoryModeChoice",
    "NRS",
    "myIrcAddresses",
    "skipAllDialogue",
    "infiniteCredits",
    "infinitePlatinum",
    "infiniteEndo",
    "infiniteRegalAya",
    "infiniteHelminthMaterials",
    "claimingBlueprintRefundsIngredients",
    "dontSubtractPurchaseCreditCost",
    "dontSubtractPurchasePlatinumCost",
    "dontSubtractPurchaseItemCost",
    "dontSubtractPurchaseStandingCost",
    "dontSubtractVoidTraces",
    "dontSubtractConsumables",
    "universalPolarityEverywhere",
    "unlockDoubleCapacityPotatoesEverywhere",
    "unlockExilusEverywhere",
    "unlockArcanesEverywhere",
    "unlockAllProfitTakerStages",
    "unlockAllSimarisResearchEntries",
    "unlockAllScans",
    "unlockAllShipFeatures",
    "unlockAllCapturaScenes",
    "noDailyStandingLimits",
    "noDailyFocusLimit",
    "noArgonCrystalDecay",
    "noMasteryRankUpCooldown",
    "noVendorPurchaseLimits",
    "noDecoBuildStage",
    "noDeathMarks",
    "noKimCooldowns",
    "syndicateMissionsRepeatable",
    "instantFinishRivenChallenge",
    "instantResourceExtractorDrones",
    "noResourceExtractorDronesDamage",
    "baroAlwaysAvailable",
    "baroFullyStocked",
    "missionsCanGiveAllRelics",
    "exceptionalRelicsAlwaysGiveBronzeReward",
    "flawlessRelicsAlwaysGiveSilverReward",
    "radiantRelicsAlwaysGiveGoldReward",
    "disableDailyTribute",
    "noDojoRoomBuildStage",
    "noDojoDecoBuildStage",
    "fastDojoRoomDestruction",
    "noDojoResearchCosts",
    "noDojoResearchTime",
    "fastClanAscension",
    "unlockAllSkins",
    "unlockAllFlavourItems",
    "unlockAllShipDecorations",
    "unlockAllDecoRecipes",
    "spoofMasteryRank",
    "relicRewardItemCountMultiplier",
    "nightwaveStandingMultiplier"
];
if (args.docker) {
    configRemovedOptionsKeys.push("bindAddress");
    configRemovedOptionsKeys.push("httpPort");
    configRemovedOptionsKeys.push("httpsPort");
}

export const configPath = path.join(repoDir, args.configPath ?? "config.json");

export const config: IConfig = {
    database: "mongodb://127.0.0.1:27017/openWF",
    logger: {
        files: true,
        level: "trace"
    },
    myAddress: "localhost"
};

export const loadConfig = (): void => {
    const newConfig = JSON.parse(fs.readFileSync(configPath, "utf-8")) as IConfig;

    // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory.
    for (const key of Object.keys(config)) {
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        (config as any)[key] = undefined;
    }

    Object.assign(config, newConfig);
};

export const getReflexiveAddress = (request: Request): { myAddress: string; myUrlBase: string } => {
    let myAddress: string;
    let myUrlBase: string = request.protocol + "://";
    if (request.host.indexOf("warframe.com") == -1) {
        // Client request was redirected cleanly, so we know it can reach us how it's reaching us now.
        myAddress = request.hostname;
        myUrlBase += request.host;
    } else {
        // Don't know how the client reached us, hoping the config does.
        myAddress = config.myAddress;
        myUrlBase += myAddress;
        const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443;
        if (port != (request.protocol == "http" ? 80 : 443)) {
            myUrlBase += ":" + port;
        }
    }
    return { myAddress, myUrlBase };
};

export const configIdToIndexable = (id: string): [Record<string, boolean | string | number | undefined>, string] => {
    let obj = config as unknown as Record<string, never>;
    const arr = id.split(".");
    while (arr.length > 1) {
        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
        obj[arr[0]] ??= {} as never;
        obj = obj[arr[0]];
        arr.splice(0, 1);
    }
    return [obj, arr[0]];
};

export interface IWebServerParams {
    address: string;
    httpPort: number;
    httpsPort: number;
    certFile: string;
    keyFile: string;
}

export const getWebServerParams = (): IWebServerParams => {
    return {
        address: config.bindAddress || "0.0.0.0",
        httpPort: config.httpPort || 80,
        httpsPort: config.httpsPort || 443,
        certFile: config.httpsCertFile || "static/cert/cert.pem",
        keyFile: config.httpsKeyFile || "static/cert/key.pem"
    };
};

export const getNrsAddresses = (): [string, number][] => {
    return (config.nrsAddresses ?? []).map(nrsAddr => {
        nrsAddr = nrsAddr.replaceAll("%THIS_MACHINE%", "127.0.0.1");
        let nrsPort = 4950;
        const colon = nrsAddr.indexOf(":");
        if (colon != -1) {
            nrsPort = parseInt(nrsAddr.substring(colon + 1));
            nrsAddr = nrsAddr.substring(0, colon);
        }
        return [nrsAddr, nrsPort];
    });
};

export const shouldDoServerQol = (
    key: TQolConfigKey,
    clientBuildLabel: string,
    introducedInBuildLabel: string
): boolean => {
    let value = config.serversideQualityOfLife?.[key];
    if (value === undefined) {
        value = configDefaults.serversideQualityOfLife[key];
    }
    if (value === null) {
        return version_compare(clientBuildLabel, introducedInBuildLabel) >= 0;
    }
    return value;
};