Compare commits
11 Commits
24d778371f
...
630d7471f2
Author | SHA1 | Date | |
---|---|---|---|
630d7471f2 | |||
eac1c11c46 | |||
8f07f8faea | |||
f242d9f873 | |||
9a034b1c8a | |||
122950034e | |||
636d3100f3 | |||
444c92f0c6 | |||
653798b987 | |||
7a88f6f486 | |||
82b203e00b |
17
AGENTS.md
Normal file
17
AGENTS.md
Normal file
@ -0,0 +1,17 @@
|
||||
## In General
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Use `npm i` or `npm ci` to install all dependencies.
|
||||
|
||||
### Testing
|
||||
|
||||
Use `npm run verify` to verify that your changes pass TypeScript's checks.
|
||||
|
||||
### Formatting
|
||||
|
||||
Use `npm run prettier` to ensure your formatting matches the expected format. Failing to do so will cause CI failure.
|
||||
|
||||
## WebUI Specific
|
||||
|
||||
The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT produce non-English strings; we want them to be translated by humans who can understand the full context.
|
@ -59,6 +59,10 @@
|
||||
"unlockAllSimarisResearchEntries": false,
|
||||
"spoofMasteryRank": -1,
|
||||
"nightwaveStandingMultiplier": 1,
|
||||
"unfaithfulBugFixes": {
|
||||
"ignore1999LastRegionPlayed": false,
|
||||
"fixXtraCheeseTimer": false
|
||||
},
|
||||
"worldState": {
|
||||
"creditBoost": false,
|
||||
"affinityBoost": false,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getCalendarProgress, getInventory } from "@/src/services/inventoryService";
|
||||
import { checkCalendarChallengeCompletion, getCalendarProgress, getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountIdForRequest } from "@/src/services/loginService";
|
||||
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
|
||||
import { getWorldState } from "@/src/services/worldStateService";
|
||||
@ -12,27 +12,23 @@ export const completeCalendarEventController: RequestHandler = async (req, res)
|
||||
const calendarProgress = getCalendarProgress(inventory);
|
||||
const currentSeason = getWorldState().KnownCalendarSeasons[0];
|
||||
let inventoryChanges: IInventoryChanges = {};
|
||||
let dayIndex = 0;
|
||||
for (const day of currentSeason.Days) {
|
||||
if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") {
|
||||
if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) {
|
||||
if (day.events.length != 0) {
|
||||
const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
|
||||
if (selection.type == "CET_REWARD") {
|
||||
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory))
|
||||
.InventoryChanges;
|
||||
} else if (selection.type == "CET_UPGRADE") {
|
||||
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
|
||||
} else if (selection.type != "CET_PLOT") {
|
||||
throw new Error(`unexpected selection type: ${selection.type}`);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
++dayIndex;
|
||||
const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
|
||||
const day = currentSeason.Days[dayIndex];
|
||||
if (day.events.length != 0) {
|
||||
if (day.events[0].type == "CET_CHALLENGE") {
|
||||
throw new Error(`completeCalendarEvent should not be used for challenges`);
|
||||
}
|
||||
const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
|
||||
if (selection.type == "CET_REWARD") {
|
||||
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)).InventoryChanges;
|
||||
} else if (selection.type == "CET_UPGRADE") {
|
||||
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
|
||||
} else if (selection.type != "CET_PLOT") {
|
||||
throw new Error(`unexpected selection type: ${selection.type}`);
|
||||
}
|
||||
}
|
||||
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
|
||||
calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
|
||||
checkCalendarChallengeCompletion(calendarProgress, currentSeason);
|
||||
await inventory.save();
|
||||
res.json({
|
||||
InventoryChanges: inventoryChanges,
|
||||
|
@ -8,16 +8,15 @@ import {
|
||||
getKnifeUpgrade,
|
||||
getNemesisManifest,
|
||||
getNemesisPasscode,
|
||||
getNemesisPasscodeModTypes,
|
||||
GUESS_CORRECT,
|
||||
GUESS_INCORRECT,
|
||||
GUESS_NEUTRAL,
|
||||
GUESS_NONE,
|
||||
GUESS_WILDCARD,
|
||||
IKnifeResponse
|
||||
IKnifeResponse,
|
||||
parseUpgrade
|
||||
} from "@/src/helpers/nemesisHelpers";
|
||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
|
||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
|
||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
|
||||
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
|
||||
import { getAccountForRequest } from "@/src/services/loginService";
|
||||
@ -215,7 +214,19 @@ export const nemesisController: RequestHandler = async (req, res) => {
|
||||
}
|
||||
];
|
||||
inventory.Nemesis!.Weakened = true;
|
||||
await consumePasscodeModCharges(inventory, response);
|
||||
|
||||
// Subtract a charge from all requiem mods installed on parazon
|
||||
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
|
||||
const dataknifeLoadout = loadout.DATAKNIFE.id(
|
||||
inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid
|
||||
);
|
||||
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
|
||||
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
|
||||
for (let i = 3; i != 6; ++i) {
|
||||
//logger.debug(`subtracting a charge from ${dataknifeUpgrades[i]}`);
|
||||
const upgrade = parseUpgrade(inventory, dataknifeUpgrades[i]);
|
||||
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Guess was incorrect, increase rank
|
||||
@ -380,18 +391,3 @@ interface IKnife {
|
||||
AttachedUpgrades: IUpgradeClient[];
|
||||
HiddenWhenHolstered: boolean;
|
||||
}
|
||||
|
||||
const consumePasscodeModCharges = async (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
response: IKnifeResponse
|
||||
): Promise<void> => {
|
||||
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
|
||||
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
|
||||
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
|
||||
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
|
||||
const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
|
||||
for (const modType of modTypes) {
|
||||
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
|
||||
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
|
||||
}
|
||||
};
|
||||
|
@ -13,7 +13,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
|
||||
|
||||
const inventory = await getInventory(
|
||||
account._id.toString(),
|
||||
"ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations"
|
||||
"ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress"
|
||||
);
|
||||
let affiliationMods: IAffiliationMods[] = [];
|
||||
if (challenges.ChallengeProgress) {
|
||||
|
@ -237,7 +237,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti
|
||||
return passcode;
|
||||
};
|
||||
|
||||
const requiemMods: readonly string[] = [
|
||||
/*const requiemMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
|
||||
@ -246,7 +246,7 @@ const requiemMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
|
||||
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod"
|
||||
];
|
||||
];*/
|
||||
|
||||
export const antivirusMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
|
||||
@ -259,12 +259,12 @@ export const antivirusMods: readonly string[] = [
|
||||
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
|
||||
];
|
||||
|
||||
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
|
||||
/*export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
|
||||
const passcode = getNemesisPasscode(nemesis);
|
||||
return nemesis.Faction == "FC_INFESTATION"
|
||||
? passcode.map(i => antivirusMods[i])
|
||||
: passcode.map(i => requiemMods[i]);
|
||||
};
|
||||
};*/
|
||||
|
||||
// Symbols; 0-7 are the normal requiem mods.
|
||||
export const GUESS_NONE = 8;
|
||||
@ -343,6 +343,27 @@ export const getKnifeUpgrade = (
|
||||
throw new Error(`${type} does not seem to be installed on parazon?!`);
|
||||
};
|
||||
|
||||
export const parseUpgrade = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
str: string
|
||||
): { ItemId: IOid; ItemType: string } => {
|
||||
if (str.length == 24) {
|
||||
const upgrade = inventory.Upgrades.id(str);
|
||||
if (upgrade) {
|
||||
return {
|
||||
ItemId: { $oid: str },
|
||||
ItemType: upgrade.ItemType
|
||||
};
|
||||
}
|
||||
throw new Error(`Could not resolve oid ${str}`);
|
||||
} else {
|
||||
return {
|
||||
ItemId: { $oid: "000000000000000000000000" },
|
||||
ItemType: str
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const consumeModCharge = (
|
||||
response: IKnifeResponse,
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
|
@ -66,6 +66,10 @@ export interface IConfig {
|
||||
unlockAllSimarisResearchEntries?: boolean;
|
||||
spoofMasteryRank?: number;
|
||||
nightwaveStandingMultiplier?: number;
|
||||
unfaithfulBugFixes?: {
|
||||
ignore1999LastRegionPlayed?: boolean;
|
||||
fixXtraCheeseTimer?: boolean;
|
||||
};
|
||||
worldState?: {
|
||||
creditBoost?: boolean;
|
||||
affinityBoost?: boolean;
|
||||
|
@ -84,9 +84,11 @@ import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from ".
|
||||
import { createMessage } from "./inboxService";
|
||||
import { getMaxStanding, getMinStanding } from "@/src/helpers/syndicateStandingHelper";
|
||||
import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService";
|
||||
import { ICalendarSeason } from "@/src/types/worldStateTypes";
|
||||
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers";
|
||||
import { TAccountDocument } from "./loginService";
|
||||
import { unixTimesInMs } from "../constants/timeConstants";
|
||||
import { addString } from "../helpers/stringHelpers";
|
||||
|
||||
export const createInventory = async (
|
||||
accountOwnerId: Types.ObjectId,
|
||||
@ -1783,6 +1785,10 @@ export const addChallenges = (
|
||||
} else {
|
||||
inventory.ChallengeProgress.push({ Name, Progress });
|
||||
}
|
||||
|
||||
if (Name.startsWith("Calendar")) {
|
||||
addString(getCalendarProgress(inventory).SeasonProgress.ActivatedChallenges, Name);
|
||||
}
|
||||
});
|
||||
|
||||
const affiliationMods: IAffiliationMods[] = [];
|
||||
@ -2029,6 +2035,20 @@ export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICal
|
||||
return inventory.CalendarProgress;
|
||||
};
|
||||
|
||||
export const checkCalendarChallengeCompletion = (
|
||||
calendarProgress: ICalendarProgress,
|
||||
currentSeason: ICalendarSeason
|
||||
): void => {
|
||||
const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
|
||||
if (calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx >= dayIndex) {
|
||||
const day = currentSeason.Days[dayIndex];
|
||||
if (day.events.length != 0 && day.events[0].type == "CET_CHALLENGE") {
|
||||
//logger.debug(`already completed the challenge, skipping ahead`);
|
||||
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const giveNemesisWeaponRecipe = (
|
||||
inventory: TInventoryDatabaseDocument,
|
||||
weaponType: string,
|
||||
|
@ -33,6 +33,7 @@ import {
|
||||
addSkin,
|
||||
addStanding,
|
||||
applyClientEquipmentUpdates,
|
||||
checkCalendarChallengeCompletion,
|
||||
combineInventoryChanges,
|
||||
generateRewardSeed,
|
||||
getCalendarProgress,
|
||||
@ -67,7 +68,15 @@ import {
|
||||
} from "@/src/helpers/nemesisHelpers";
|
||||
import { Loadout } from "../models/inventoryModels/loadoutModel";
|
||||
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
|
||||
import { getLiteSortie, getSortie, idToBountyCycle, idToDay, idToWeek, pushClassicBounties } from "./worldStateService";
|
||||
import {
|
||||
getLiteSortie,
|
||||
getSortie,
|
||||
getWorldState,
|
||||
idToBountyCycle,
|
||||
idToDay,
|
||||
idToWeek,
|
||||
pushClassicBounties
|
||||
} from "./worldStateService";
|
||||
import { config } from "./configService";
|
||||
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
|
||||
import { ISyndicateMissionInfo } from "../types/worldStateTypes";
|
||||
@ -259,7 +268,9 @@ export const addMissionInventoryUpdates = async (
|
||||
addMissionComplete(inventory, value);
|
||||
break;
|
||||
case "LastRegionPlayed":
|
||||
inventory.LastRegionPlayed = value;
|
||||
if (!(config.unfaithfulBugFixes?.ignore1999LastRegionPlayed && value === "1999MapName")) {
|
||||
inventory.LastRegionPlayed = value;
|
||||
}
|
||||
break;
|
||||
case "RawUpgrades":
|
||||
addMods(inventory, value);
|
||||
@ -620,12 +631,11 @@ export const addMissionInventoryUpdates = async (
|
||||
}
|
||||
case "CalendarProgress": {
|
||||
const calendarProgress = getCalendarProgress(inventory);
|
||||
for (const progress of value) {
|
||||
const challengeName = progress.challenge.substring(progress.challenge.lastIndexOf("/") + 1);
|
||||
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
|
||||
calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx++;
|
||||
calendarProgress.SeasonProgress.ActivatedChallenges.push(challengeName);
|
||||
}
|
||||
const currentSeason = getWorldState().KnownCalendarSeasons[0];
|
||||
calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx = currentSeason.Days.findIndex(
|
||||
x => x.events[0].challenge == value[value.length - 1].challenge
|
||||
);
|
||||
checkCalendarChallengeCompletion(calendarProgress, currentSeason);
|
||||
break;
|
||||
}
|
||||
case "duviriCaveOffers": {
|
||||
@ -1629,7 +1639,19 @@ function getRandomMissionDrops(
|
||||
}
|
||||
rewardManifests = [job.rewards];
|
||||
if (job.xpAmounts.length > 1) {
|
||||
rotations = [RewardInfo.JobStage! % (job.xpAmounts.length - 1)];
|
||||
const curentStage = RewardInfo.JobStage! + 1;
|
||||
const totalStage = job.xpAmounts.length;
|
||||
let tableIndex = 1; // Stage 2, Stage 3 of 4, and Stage 3 of 5
|
||||
|
||||
if (curentStage == 1) {
|
||||
tableIndex = 0;
|
||||
} else if (curentStage == totalStage) {
|
||||
tableIndex = 3;
|
||||
} else if (totalStage == 5 && curentStage == 4) {
|
||||
tableIndex = 2;
|
||||
}
|
||||
|
||||
rotations = [tableIndex];
|
||||
} else {
|
||||
rotations = [0];
|
||||
}
|
||||
@ -1638,11 +1660,7 @@ function getRandomMissionDrops(
|
||||
(RewardInfo.JobStage === job.xpAmounts.length - 1 || job.isVault) &&
|
||||
!isEndlessJob
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (ExportRewards[job.rewards]) {
|
||||
rewardManifests.push(job.rewards);
|
||||
rotations.push(ExportRewards[job.rewards].length - 1);
|
||||
}
|
||||
rotations.push(3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import { Account } from "../models/loginModel";
|
||||
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "./loginService";
|
||||
import { IDatabaseAccountJson } from "../types/loginTypes";
|
||||
import { HydratedDocument } from "mongoose";
|
||||
import { Agent, WebSocket } from "undici";
|
||||
import { Agent, WebSocket as UnidiciWebSocket } from "undici";
|
||||
|
||||
let httpServer: http.Server | undefined;
|
||||
let httpsServer: https.Server | undefined;
|
||||
@ -46,35 +46,45 @@ export const startWebServer = (): void => {
|
||||
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
|
||||
);
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/20547
|
||||
if (!process.versions.bun) {
|
||||
void runWsSelfTest("wss", httpsPort).then(ok => {
|
||||
if (!ok) {
|
||||
void runWsSelfTest("wss", httpsPort).then(ok => {
|
||||
if (!ok) {
|
||||
logger.warn(`WSS self-test failed. The server may not actually be reachable at port ${httpsPort}.`);
|
||||
if (process.platform == "win32") {
|
||||
logger.warn(
|
||||
`WSS self-test failed. The server may not actually be reachable at port ${httpsPort}.`
|
||||
`You can check who actually has that port via powershell: Get-Process -Id (Get-NetTCPConnection -LocalPort ${httpsPort}).OwningProcess`
|
||||
);
|
||||
if (process.platform == "win32") {
|
||||
logger.warn(
|
||||
`You can check who actually has that port via powershell: Get-Process -Id (Get-NetTCPConnection -LocalPort ${httpsPort}).OwningProcess`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const runWsSelfTest = (protocol: "ws" | "wss", port: number): Promise<boolean> => {
|
||||
return new Promise(resolve => {
|
||||
const agent = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
const client = new WebSocket(`${protocol}://localhost:${port}/custom/selftest`, { dispatcher: agent });
|
||||
client.onmessage = (e): void => {
|
||||
resolve(e.data == "SpaceNinjaServer");
|
||||
};
|
||||
client.onerror = client.onclose = (): void => {
|
||||
resolve(false);
|
||||
};
|
||||
// https://github.com/oven-sh/bun/issues/20547
|
||||
if (process.versions.bun) {
|
||||
const client = new WebSocket(`${protocol}://localhost:${port}/custom/selftest`, {
|
||||
tls: { rejectUnauthorized: false }
|
||||
} as unknown as string);
|
||||
client.onmessage = (e): void => {
|
||||
resolve(e.data == "SpaceNinjaServer");
|
||||
};
|
||||
client.onerror = client.onclose = (): void => {
|
||||
resolve(false);
|
||||
};
|
||||
} else {
|
||||
const agent = new Agent({ connect: { rejectUnauthorized: false } });
|
||||
const client = new UnidiciWebSocket(`${protocol}://localhost:${port}/custom/selftest`, {
|
||||
dispatcher: agent
|
||||
});
|
||||
client.onmessage = (e): void => {
|
||||
resolve(e.data == "SpaceNinjaServer");
|
||||
};
|
||||
client.onerror = client.onclose = (): void => {
|
||||
resolve(false);
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1327,6 +1327,17 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
const cheeseInterval = hourInSeconds * 8;
|
||||
const cheeseDuration = hourInSeconds * 2;
|
||||
const cheeseIndex = Math.trunc(timeSecs / cheeseInterval);
|
||||
let cheeseStart = cheeseIndex * cheeseInterval;
|
||||
let cheeseEnd = cheeseStart + cheeseDuration;
|
||||
let cheeseNext = (cheeseIndex + 1) * cheeseInterval;
|
||||
// Live servers only update the start time once it happens, which makes the
|
||||
// client show a negative countdown during off-hours. Optionally adjust the
|
||||
// times so the next activation is always in the future.
|
||||
if (config.unfaithfulBugFixes?.fixXtraCheeseTimer && timeSecs >= cheeseEnd) {
|
||||
cheeseStart = cheeseNext;
|
||||
cheeseEnd = cheeseStart + cheeseDuration;
|
||||
cheeseNext += cheeseInterval;
|
||||
}
|
||||
const tmp: ITmp = {
|
||||
cavabegin: "1690761600",
|
||||
PurchasePlatformLockEnabled: true,
|
||||
@ -1351,9 +1362,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
|
||||
ennnd: true,
|
||||
mbrt: true,
|
||||
fbst: {
|
||||
a: cheeseIndex * cheeseInterval, // This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live.
|
||||
e: cheeseIndex * cheeseInterval + cheeseDuration,
|
||||
n: (cheeseIndex + 1) * cheeseInterval
|
||||
a: cheeseStart,
|
||||
e: cheeseEnd,
|
||||
n: cheeseNext
|
||||
},
|
||||
sfn: [550, 553, 554, 555][halfHour % 4]
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user