isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
178 lines (153 loc) • 5.57 kB
text/typescript
import type { CopyableIsaacAPIClassType } from "isaac-typescript-definitions";
import { game } from "../core/cachedClasses";
import { SerializationBrand } from "../enums/private/SerializationBrand";
import type { ReadonlyRecord } from "../types/ReadonlyRecord";
import { traceback } from "./debugFunctions";
import { isaacAPIClassEquals, isIsaacAPIClassOfType } from "./isaacAPIClass";
import { logError } from "./log";
import { getNumbersFromTable, tableHasKeys } from "./table";
import { isTable } from "./types";
import { assertDefined } from "./utils";
export type SerializedRNG = LuaMap<string, unknown> & {
readonly __serializedRNGBrand: symbol;
readonly __kind: CopyableIsaacAPIClassType.RNG;
};
/**
* This is the ShiftIdx that Blade recommended after having reviewing the game's internal functions.
* Any value between 0 and 80 should work equally well.
*
* @see https://www.jstatsoft.org/article/view/v008i14/xorshift.pdf
*/
const RECOMMENDED_SHIFT_IDX = 35;
const OBJECT_NAME = "RNG";
const KEYS = ["seed"] as const;
/** Helper function to copy an `RNG` Isaac API class. */
export function copyRNG(rng: RNG): RNG {
if (!isRNG(rng)) {
error(
`Failed to copy a ${OBJECT_NAME} object since the provided object was not a userdata ${OBJECT_NAME} class.`,
);
}
const seed = rng.GetSeed();
return newRNG(seed);
}
/**
* Helper function to convert a `SerializedRNG` object to a normal `RNG` object. (This is used by
* the save data manager when reading data from the "save#.dat" file.)
*/
export function deserializeRNG(rng: SerializedRNG): RNG {
if (!isTable(rng)) {
error(
`Failed to deserialize a ${OBJECT_NAME} object since the provided object was not a Lua table.`,
);
}
const [seed] = getNumbersFromTable(rng, OBJECT_NAME, ...KEYS);
assertDefined(
seed,
`Failed to deserialize a ${OBJECT_NAME} object since the provided object did not have a value for: seed`,
);
return newRNG(seed as Seed);
}
/**
* Helper function to get a random `Seed` value to be used in spawning entities and so on. Use this
* instead of calling the `Random` function directly since that can return a value of 0 and crash
* the game.
*/
export function getRandomSeed(): Seed {
const randomNumber = Random(); // eslint-disable-line @typescript-eslint/no-deprecated
const safeRandomNumber = randomNumber === 0 ? 1 : randomNumber;
return safeRandomNumber as Seed;
}
/** Helper function to check if something is an instantiated `RNG` object. */
export function isRNG(object: unknown): object is RNG {
return isIsaacAPIClassOfType(object, OBJECT_NAME);
}
/**
* Used to determine is the given table is a serialized `RNG` object created by the `deepCopy`
* function.
*/
export function isSerializedRNG(object: unknown): object is SerializedRNG {
if (!isTable(object)) {
return false;
}
return tableHasKeys(object, ...KEYS) && object.has(SerializationBrand.RNG);
}
/**
* Helper function to initialize a new RNG object using Blade's recommended shift index.
*
* @param seed Optional. The seed to initialize it with. Default is a random seed.
*/
export function newRNG(seed = getRandomSeed()): RNG {
const rng = RNG();
setSeed(rng, seed);
return rng;
}
export function rngEquals(rng1: RNG, rng2: RNG): boolean {
return isaacAPIClassEquals(rng1, rng2, KEYS);
}
/**
* Helper function to convert a `RNG` object to a `SerializedRNG` object. (This is used by the save
* data manager when writing data from the "save#.dat" file.)
*/
export function serializeRNG(rng: RNG): SerializedRNG {
if (!isRNG(rng)) {
error(
`Failed to serialize a ${OBJECT_NAME} object since the provided object was not a userdata ${OBJECT_NAME} class.`,
);
}
const seed = rng.GetSeed();
const rngTable = new LuaMap<string, unknown>();
rngTable.set("seed", seed);
rngTable.set(SerializationBrand.RNG, "");
return rngTable as SerializedRNG;
}
/**
* Helper function to iterate over the provided object and set the seed for all of the values that
* are RNG objects equal to a particular seed.
*/
export function setAllRNGToSeed(
object: ReadonlyRecord<string, RNG>,
seed: Seed,
): void {
if (!isTable(object)) {
error(
`Failed to iterate over the object containing RNG objects since the type of the provided object was: ${typeof object}`,
);
}
let setAtLeastOneSeed = false;
for (const [_key, value] of object) {
if (isRNG(value)) {
setSeed(value, seed);
setAtLeastOneSeed = true;
}
}
if (!setAtLeastOneSeed) {
error(
`Failed to set all RNG objects to seed ${seed} because the parent object did not contain any RNG objects.`,
);
}
}
/**
* Helper function to iterate over the provided object and set the seed for all of the values that
* are RNG objects equal to the start seed for the current run.
*/
export function setAllRNGToStartSeed(
object: ReadonlyRecord<string, RNG>,
): void {
const seeds = game.GetSeeds();
const startSeed = seeds.GetStartSeed();
setAllRNGToSeed(object, startSeed);
}
/** Helper function to set a seed to an RNG object using Blade's recommended shift index. */
export function setSeed(rng: RNG, seed: Seed): void {
if (seed === 0) {
seed = getRandomSeed();
logError(
"Failed to set a RNG object to a seed of 0. Using a random value instead.",
);
traceback();
}
// The game expects seeds in the range of 1 to 4294967295 (1^32 - 1).
// eslint-disable-next-line @typescript-eslint/no-deprecated
rng.SetSeed(seed, RECOMMENDED_SHIFT_IDX);
}