isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
650 lines (560 loc) • 18.9 kB
text/typescript
import type {
CollectibleType,
ItemPoolType,
} from "isaac-typescript-definitions";
import {
BossID,
DamageFlag,
DisplayFlag,
EntityFlag,
GameStateFlag,
GridRoom,
HeartSubType,
LevelStateFlag,
Music,
NullItemID,
ProjectileFlag,
RoomType,
SeedEffect,
SoundEffect,
StageID,
TearFlag,
UseFlag,
} from "isaac-typescript-definitions";
import { game, musicManager, sfxManager } from "../core/cachedClasses";
import type { ReadonlyMap } from "../types/ReadonlyMap";
import type { ReadonlyRecord } from "../types/ReadonlyRecord";
import { ReadonlySet } from "../types/ReadonlySet";
import { arrayToString, isArray } from "./array";
import { getBossID } from "./bosses";
import { getCollectibleName } from "./collectibles";
import { getEntityID } from "./entities";
import { getEnumEntries } from "./enums";
import { hasFlag } from "./flag";
import { getIsaacAPIClassName } from "./isaacAPIClass";
import { getItemPoolName } from "./itemPool";
import { log } from "./log";
import { getEffectsList } from "./playerEffects";
import { getPlayerHealth } from "./playerHealth";
import { getPlayerName } from "./players";
import { getRoomData, getRoomGridIndex, getRoomListIndex } from "./roomData";
import { combineSets, getSortedSetValues } from "./set";
import { sortNormal } from "./sort";
import { iterateTableInOrder } from "./table";
import { getTrinketName } from "./trinkets";
import { isDefaultMap, isTSTLMap, isTSTLSet } from "./tstlClass";
import { isTable, isUserdata } from "./types";
import { vectorToString } from "./vector";
/**
* Helper function to log all of the values in an array.
*
* @param array The array to log.
* @param name Optional. The name of the array, which will be logged before the elements.
*/
export function logArray(
this: void,
array: readonly unknown[],
name?: string,
): void {
name ??= "array";
// We do not assume the given array has contiguous values in order to be more permissive about the
// kinds of arrays that will successfully log without a run-time error.
if (!isArray(array, false)) {
log("Tried to log an array, but the given object was not an array.");
return;
}
const arrayString = arrayToString(array);
log(`Logging ${name}: ${arrayString}`);
}
/**
* Helper function to log the names of a collectible type array.
*
* @param collectibleTypes The collectible types to log.
* @param name Optional. The name of the array, which will be logged before the elements.
*/
export function logCollectibleTypes(
this: void,
collectibleTypes: readonly CollectibleType[],
name?: string,
): void {
name ??= "collectibles";
log(`Logging ${name}:`);
let i = 1;
for (const collectibleType of collectibleTypes) {
const collectibleName = getCollectibleName(collectibleType);
log(`${i}) ${collectibleName} (${collectibleType})`);
i++;
}
}
/**
* Helper function to log a `Color` object.
*
* @param color The `Color` object to log.
* @param name Optional. The name of the object, which will be logged before the properties.
*/
export function logColor(this: void, color: Color, name?: string): void {
name ??= "color";
log(
`Logging ${name}: R${color.R}, G${color.G}, B${color.B}, A${color.A}, RO${color.RO}, BO${color.BO}, GO${color.GO}`,
);
}
/** Helper function to log every damage flag that is turned on. Useful when debugging. */
export function logDamageFlags(
this: void,
damageFlags: DamageFlag | BitFlags<DamageFlag>,
): void {
logFlags(damageFlags, DamageFlag, "damage");
}
/** Helper function to log every display flag that is turned on. Useful when debugging. */
export function logDisplayFlags(
this: void,
displayFlags: DisplayFlag | BitFlags<DisplayFlag>,
): void {
logFlags(displayFlags, DisplayFlag, "display");
}
/** Helper function to log every entity flag that is turned on. Useful when debugging. */
export function logEntityFlags(
this: void,
entityFlags: EntityFlag | BitFlags<EntityFlag>,
): void {
logFlags(entityFlags, EntityFlag, "entity");
}
export function logEntityID(this: void, entity: Entity): void {
const entityID = getEntityID(entity);
log(`Logging entity: ${entityID}`);
}
/** Helper function for logging every flag that is turned on. Useful when debugging. */
export function logFlags<T extends BitFlag | BitFlag128>(
this: void,
flags: T | BitFlags<T>,
flagEnum: ReadonlyRecord<string, T>,
description = "",
): void {
if (description !== "") {
description = "flag";
}
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(`Logging ${description} values for: ${flags}`);
let hasNoFlags = true;
const entries = getEnumEntries(flagEnum);
for (const [key, value] of entries) {
if (hasFlag(flags, value)) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(` Has flag: ${key} (${value})`);
hasNoFlags = false;
}
}
if (hasNoFlags) {
log(" n/a (no flags)");
}
}
/** Helper function for logging every game state flag that is turned on. Useful when debugging. */
export function logGameStateFlags(this: void): void {
log("Logging game state flags:");
const gameStateFlagEntries = getEnumEntries(GameStateFlag);
let hasNoFlags = true;
for (const [key, gameStateFlag] of gameStateFlagEntries) {
const flagValue = game.GetStateFlag(gameStateFlag);
if (flagValue) {
log(` Has flag: ${key} (${gameStateFlag})`);
hasNoFlags = false;
}
}
if (hasNoFlags) {
log(" n/a (no flags)");
}
}
/**
* Helper function to log the names of a item pool type array.
*
* @param itemPoolTypes The item pool types to log.
* @param name Optional. The name of the array, which will be logged before the elements.
*/
export function logItemPoolTypes(
this: void,
itemPoolTypes: readonly ItemPoolType[],
name?: string,
): void {
name ??= "item pool types";
log(`Logging ${name}:`);
let i = 1;
for (const itemPoolType of itemPoolTypes) {
const itemPoolName = getItemPoolName(itemPoolType);
log(`${i}) ${itemPoolName} (${itemPoolType})`);
i++;
}
}
/**
* Helper function to log a `KColor` object.
*
* @param kColor The `KColor` object to log.
* @param name Optional. The name of the object, which will be logged before the properties.
*/
export function logKColor(this: void, kColor: KColor, name?: string): void {
name ??= "KColor";
log(
`Logging ${name}: R${kColor.Red}, G${kColor.Green}, B${kColor.Blue}, A${kColor.Alpha}`,
);
}
/** Helper function for logging every level state flag that is turned on. Useful when debugging. */
export function logLevelStateFlags(this: void): void {
const level = game.GetLevel();
const levelStateFlagEntries = getEnumEntries(LevelStateFlag);
log("Logging level state flags:");
let hasNoFlags = true;
for (const [key, levelStateFlag] of levelStateFlagEntries) {
const flagValue = level.GetStateFlag(levelStateFlag);
if (flagValue) {
log(` Has flag: ${key} (${levelStateFlag})`);
hasNoFlags = false;
}
}
if (hasNoFlags) {
log(" n/a (no flags)");
}
}
/**
* Helper function to log a TSTL `Map`.
*
* @param map The TSTL `Map` to log.
* @param name Optional. The name of the map, which will be logged before the elements.
*/
export function logMap(
this: void,
map: ReadonlyMap<number | string, unknown>,
name?: string,
): void {
if (!isTSTLMap(map) && !isDefaultMap(map)) {
log("Tried to log a TSTL map, but the given object was not a TSTL map.");
return;
}
const suffix = name === undefined ? "" : ` "${name}"`;
log(`Logging a TSTL map${suffix}:`);
const mapKeys = [...map.keys()];
mapKeys.sort(sortNormal);
for (const key of mapKeys) {
const value = map.get(key);
log(` ${key} --> ${value}`);
}
log(` The size of the map was: ${map.size}`);
}
export function logMusic(this: void): void {
const currentMusic = musicManager.GetCurrentMusicID();
log(
`Currently playing music track: ${Music[currentMusic]} (${currentMusic})`,
);
}
export function logPlayerEffects(this: void, player: EntityPlayer): void {
const effects = getEffectsList(player);
log("Logging player effects:");
if (effects.length === 0) {
log(" n/a (no effects)");
return;
}
for (const [i, effect] of effects.entries()) {
let effectDescription: string;
if (effect.Item.IsCollectible()) {
const collectibleName = getCollectibleName(effect.Item.ID);
effectDescription = `Collectible: ${collectibleName}`;
} else if (effect.Item.IsTrinket()) {
const trinketName = getTrinketName(effect.Item.ID);
effectDescription = `Trinket: ${trinketName}`;
} else if (effect.Item.IsNull()) {
const nullItemName = NullItemID[effect.Item.ID];
effectDescription = `Null item: ${nullItemName}`;
} else {
effectDescription = `Unknown type of effect: ${effect.Item.ID}`;
}
log(
` ${i + 1}) ${effectDescription} (${effect.Item.ID}) x${effect.Count}`,
);
}
}
export function logPlayerHealth(this: void, player: EntityPlayer): void {
const playerName = getPlayerName(player);
const playerHealth = getPlayerHealth(player);
log(`Player health for ${playerName}:`);
log(` Max hearts: ${playerHealth.maxHearts}`);
log(` Hearts: ${playerHealth.hearts}`);
log(` Eternal hearts: ${playerHealth.eternalHearts}`);
log(` Soul hearts: ${playerHealth.soulHearts}`);
log(` Bone hearts: ${playerHealth.boneHearts}`);
log(` Golden hearts: ${playerHealth.goldenHearts}`);
log(` Rotten hearts: ${playerHealth.rottenHearts}`);
log(` Broken hearts: ${playerHealth.brokenHearts}`);
log(` Soul charges: ${playerHealth.soulCharges}`);
log(` Blood charges: ${playerHealth.bloodCharges}`);
log(" Soul heart types: [");
for (const soulHeartType of playerHealth.soulHeartTypes) {
log(` HeartSubType.${HeartSubType[soulHeartType]}`);
}
log(" ]");
}
/** Helper function for logging every projectile flag that is turned on. Useful when debugging. */
export function logProjectileFlags(
this: void,
projectileFlags: ProjectileFlag | BitFlags<ProjectileFlag>,
): void {
logFlags(projectileFlags, ProjectileFlag, "projectile");
}
/** Helper function for logging information about the current room. */
export function logRoom(this: void): void {
const bossID = getBossID();
const roomGridIndex = getRoomGridIndex();
const roomListIndex = getRoomListIndex();
const roomData = getRoomData();
log("Logging room information:");
log(`- Room stage ID: ${StageID[roomData.StageID]} (roomData.StageID)`);
log(`- Room type: ${RoomType[roomData.Type]} (${roomData.Type})`);
log(`- Variant: ${roomData.Variant}`);
log(`- Sub-type: ${roomData.Subtype}`);
log(`- Name: ${roomData.Name}`);
const roomGridIndexName = GridRoom[roomGridIndex];
if (roomGridIndexName === undefined) {
log(`- Grid index: ${roomGridIndex}`);
} else {
log(`- Grid index: ${roomGridIndexName} (${roomGridIndex})`);
}
log(`- List index: ${roomListIndex}`);
if (bossID === undefined) {
log("- Boss ID: undefined");
} else {
log(`- Boss ID: ${BossID[bossID]} (${bossID})`);
}
}
/**
* Helper function for logging every seed effect (i.e. Easter Egg) that is turned on for the
* particular run.
*/
export function logSeedEffects(this: void): void {
const seeds = game.GetSeeds();
const seedEffectEntries = getEnumEntries(SeedEffect);
log("Logging seed effects:");
let hasNoSeedEffects = true;
for (const [key, seedEffect] of seedEffectEntries) {
if (seeds.HasSeedEffect(seedEffect)) {
log(` ${key} (${seedEffect})`);
hasNoSeedEffects = false;
}
}
if (hasNoSeedEffects) {
log(" n/a (no seed effects)");
}
}
/**
* Helper function to log a TSTL `Set`.
*
* @param set The TSTL `Set` to log.
* @param name Optional. The name of the set, which will be logged before the elements.
*/
export function logSet(
this: void,
set: ReadonlySet<number | string>,
name?: string,
): void {
if (!isTSTLSet(set)) {
log("Tried to log a TSTL set, but the given object was not a TSTL set.");
return;
}
const suffix = name === undefined ? "" : ` "${name}"`;
log(`Logging a TSTL set${suffix}:`);
const setValues = getSortedSetValues(set);
for (const value of setValues) {
log(` Value: ${value}`);
}
log(` The size of the set was: ${set.size}`);
}
/** Helper function for logging every sound effect that is currently playing. */
export function logSounds(this: void): void {
const soundEffects = getEnumEntries(SoundEffect);
for (const [key, soundEffect] of soundEffects) {
if (sfxManager.IsPlaying(soundEffect)) {
log(`Currently playing sound effect: ${key} (${soundEffect})`);
}
}
}
/**
* Helper function for logging every key and value of a Lua table. This is a deep log; the function
* will recursively call itself if it encounters a table within a table.
*
* This function will only work on tables that have string keys (because it logs the keys in order,
* instead of randomly). It will throw a run-time error if it encounters a non-string key.
*
* In order to prevent infinite recursion, this function will not log a sub-table when there are 10
* or more parent tables.
*/
export function logTable(
this: void,
luaTable: unknown,
parentTables = 0,
): void {
if (parentTables === 0) {
log("Logging a Lua table:", false);
} else if (parentTables > 10) {
return;
}
const numSpaces = (parentTables + 1) * 2; // 2, 4, 6, etc.
const indentation = " ".repeat(numSpaces);
if (!isTable(luaTable)) {
log(
`${indentation}n/a (encountered a variable of type "${typeof luaTable}" instead of a table)`,
false,
);
return;
}
let numElements = 0;
iterateTableInOrder(luaTable, (key, value) => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(`${indentation}${key} --> ${value}`, false);
if (isTable(value)) {
if (key === "__class") {
log(
`${indentation} (skipping enumerating this key to avoid infinite recursion)`,
false,
);
} else {
logTable(value, parentTables + 1);
}
}
numElements++;
});
log(`${indentation}The size of the table was: ${numElements}`, false);
}
/**
* Helper function to log the differences between the entries of two tables. Note that this will
* only do a shallow comparison.
*/
export function logTableDifferences<K extends AnyNotNil, V>(
this: void,
table1: LuaMap<K, V>,
table2: LuaMap<K, V>,
): void {
log("Comparing two Lua tables:");
const table1Keys = Object.keys(table1);
const table1KeysSet = new ReadonlySet(table1Keys);
const table2Keys = Object.keys(table2);
const table2KeysSet = new ReadonlySet(table2Keys);
const keysSet = combineSets(table1KeysSet, table2KeysSet);
const keys = [...keysSet.values()];
keys.sort(); // eslint-disable-line @typescript-eslint/require-array-sort-compare
for (const key of keys) {
const value1 = table1.get(key);
const value2 = table2.get(key);
if (value1 === undefined) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(` Table 1 is missing key: ${key}`);
}
if (value2 === undefined) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(` Table 2 is missing key: ${key}`);
}
if (value1 !== value2) {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(` ${key} --> "${value1}" versus "${value2}"`);
}
}
}
/**
* Helper function to log the keys of a Lua table. This is not a deep log; only the keys of the
* top-most table will be logged.
*
* This function is useful for tables that have recursive references.
*/
export function logTableKeys(this: void, luaTable: unknown): void {
log("Logging the keys of a Lua table:");
if (!isTable(luaTable)) {
log(
` n/a (encountered a variable of type "${typeof luaTable}" instead of a table)`,
);
return;
}
let numElements = 0;
iterateTableInOrder(luaTable, (key) => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(`${key}`);
numElements++;
});
log(` The size of the table was: ${numElements}`);
}
/**
* Helper function to log every table key and value. This is a shallow log; the function will not
* recursively traverse sub-tables.
*
* This function will only work on tables that have string keys (because it logs the keys in order,
* instead of randomly). It will throw a run-time error if it encounters a non-string key.
*/
export function logTableShallow<K extends AnyNotNil, V>(
this: void,
luaTable: LuaMap<K, V>,
): void {
log("Logging a Lua table (shallow):", false);
if (!isTable(luaTable)) {
log(
`n/a (encountered a variable of type "${typeof luaTable}" instead of a table)`,
false,
);
return;
}
let numElements = 0;
const indentation = " ";
iterateTableInOrder(luaTable, (key, value) => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
log(`${indentation}${key} --> ${value}`, false);
numElements++;
});
log(`${indentation}The size of the table was: ${numElements}`, false);
}
/** Helper function for log every tear flag that is turned on. Useful when debugging. */
export function logTearFlags(
this: void,
tearFlags: TearFlag | BitFlags<TearFlag>,
): void {
logFlags(tearFlags, TearFlag, "tear");
}
/** Helper function for printing out every use flag that is turned on. Useful when debugging. */
export function logUseFlags(
this: void,
useFlags: UseFlag | BitFlags<UseFlag>,
): void {
logFlags(useFlags, UseFlag, "use");
}
/**
* Helper function to enumerate all of the properties of a "userdata" object (i.e. an object from
* the Isaac API).
*/
export function logUserdata(this: void, userdata: unknown): void {
if (!isUserdata(userdata)) {
log("Userdata: [not userdata]");
return;
}
const metatable = getmetatable(userdata);
if (metatable === undefined) {
log("Userdata: [no metatable]");
return;
}
const classType = getIsaacAPIClassName(userdata);
if (classType === undefined) {
log("Userdata: [no class type]");
} else {
log(`Userdata: ${classType}`);
}
logTable(metatable);
}
/**
* Helper function to log a `Vector` object.
*
* @param vector The `Vector` object to log.
* @param name Optional. The name of the object, which will be logged before the properties.
* @param round Optional. If true, will round the vector values to the nearest integer. Default is
* false.
*/
export function logVector(
this: void,
vector: Vector,
name?: string,
round = false,
): void {
name ??= "vector";
const vectorString = vectorToString(vector, round);
log(`Logging ${name}: ${vectorString}`);
}