Fix NaN detection

Also removes the proxy around player and cleaned up types
This commit is contained in:
thepaperpilot 2022-12-28 09:03:51 -06:00
parent 8c8f7f7904
commit f5a25b2c2d
9 changed files with 130 additions and 158 deletions

View file

@ -14,9 +14,12 @@
</div>
<br />
<div>
<a :href="discordLink" class="nan-modal-discord-link">
<a
:href="discordLink || 'https://discord.gg/WzejVAx'"
class="nan-modal-discord-link"
>
<span class="material-icons nan-modal-discord">discord</span>
{{ discordName }}
{{ discordName || "The Paper Pilot Community" }}
</a>
</div>
<br />
@ -50,49 +53,51 @@ import state from "game/state";
import type { DecimalSource } from "util/bignum";
import Decimal, { format } from "util/bignum";
import type { ComponentPublicInstance } from "vue";
import { computed, ref, toRef } from "vue";
import { computed, ref, toRef, watch } from "vue";
import Toggle from "./fields/Toggle.vue";
import SavesManager from "./SavesManager.vue";
const { discordName, discordLink } = projInfo;
const autosave = toRef(player, "autosave");
const autosave = ref(true);
const isPaused = ref(true);
const hasNaN = toRef(state, "hasNaN");
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
const path = computed(() => state.NaNPath?.join("."));
const property = computed(() => state.NaNPath?.slice(-1)[0]);
const previous = computed<DecimalSource | null>(() => {
if (state.NaNReceiver && property.value != null) {
return state.NaNReceiver[property.value] as DecimalSource;
}
return null;
});
const isPaused = computed({
get() {
return player.devSpeed === 0;
},
set(value: boolean) {
player.devSpeed = value ? null : 0;
watch(hasNaN, hasNaN => {
if (hasNaN) {
autosave.value = player.autosave;
isPaused.value = player.devSpeed === 0;
} else {
player.autosave = autosave.value;
player.devSpeed = isPaused.value ? 0 : null;
}
});
const path = computed(() => state.NaNPath?.join("."));
const previous = computed<DecimalSource | null>(() => {
if (state.NaNPersistent != null) {
return state.NaNPersistent.value;
}
return null;
});
function setZero() {
if (state.NaNReceiver && property.value != null) {
state.NaNReceiver[property.value] = new Decimal(0);
if (state.NaNPersistent != null) {
state.NaNPersistent.value = new Decimal(0);
state.hasNaN = false;
}
}
function setOne() {
if (state.NaNReceiver && property.value != null) {
state.NaNReceiver[property.value] = new Decimal(1);
if (state.NaNPersistent) {
state.NaNPersistent.value = new Decimal(1);
state.hasNaN = false;
}
}
function ignore() {
if (state.NaNReceiver && property.value != null) {
state.NaNReceiver[property.value] = new Decimal(NaN);
if (state.NaNPersistent) {
state.NaNPersistent.value = new Decimal(NaN);
state.hasNaN = false;
}
}

View file

@ -59,11 +59,10 @@
<script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json";
import type { PlayerData } from "game/player";
import type { Player } from "game/player";
import player, { stringifySave } from "game/player";
import settings from "game/settings";
import LZString from "lz-string";
import { ProxyState } from "util/proxies";
import { getUniqueID, loadSave, newSave, save } from "util/save";
import type { ComponentPublicInstance } from "vue";
import { computed, nextTick, ref, shallowReactive, watch } from "vue";
@ -72,7 +71,7 @@ import Select from "./fields/Select.vue";
import Text from "./fields/Text.vue";
import Save from "./Save.vue";
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
const isOpen = ref(false);
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null);
@ -195,7 +194,7 @@ const saves = computed(() =>
function exportSave(id: string) {
let saveToExport;
if (player.id === id) {
saveToExport = stringifySave(player[ProxyState]);
saveToExport = stringifySave(player);
} else {
saveToExport = JSON.stringify(saves.value[id]);
}
@ -228,7 +227,7 @@ function duplicateSave(id: string) {
}
const playerData = { ...saves.value[id], id: getUniqueID() };
save(playerData as PlayerData);
save(playerData as Player);
settings.saves.push(playerData.id);
}
@ -272,7 +271,7 @@ function newFromPreset(preset: string) {
}
const playerData = JSON.parse(preset);
playerData.id = getUniqueID();
save(playerData as PlayerData);
save(playerData as Player);
settings.saves.push(playerData.id);
@ -287,7 +286,7 @@ function editSave(id: string, newName: string) {
player.name = newName;
save();
} else {
save(currSave as PlayerData);
save(currSave as Player);
cachedSaves[id] = undefined;
}
}

View file

@ -6,7 +6,7 @@ import { branchedResetPropagation, createTree } from "features/trees/tree";
import { globalBus } from "game/events";
import type { BaseLayer, GenericLayer } from "game/layers";
import { createLayer } from "game/layers";
import type { PlayerData } from "game/player";
import type { Player } from "game/player";
import player from "game/player";
import type { DecimalSource } from "util/bignum";
import Decimal, { format, formatTime } from "util/bignum";
@ -79,7 +79,7 @@ export const main = createLayer("main", function (this: BaseLayer) {
*/
export const getInitialLayers = (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
player: Partial<PlayerData>
player: Partial<Player>
): Array<GenericLayer> => [main, prestige];
/**
@ -97,7 +97,7 @@ export const hasWon = computed(() => {
/* eslint-disable @typescript-eslint/no-unused-vars */
export function fixOldSave(
oldVersion: string | undefined,
player: Partial<PlayerData>
player: Partial<Player>
// eslint-disable-next-line @typescript-eslint/no-empty-function
): void {}
/* eslint-enable @typescript-eslint/no-unused-vars */

View file

@ -7,6 +7,8 @@ import Decimal from "util/bignum";
import { ProxyState } from "util/proxies";
import type { Ref, WritableComputedRef } from "vue";
import { computed, isReactive, isRef, ref } from "vue";
import player from "./player";
import state from "./state";
/**
* A symbol used in {@link Persistent} objects.
@ -56,6 +58,7 @@ export type State =
* A {@link Ref} that has been augmented with properties to allow it to be saved and loaded within the player save data object.
*/
export type Persistent<T extends State = State> = Ref<T> & {
value: T;
/** A flag that this is a persistent property. Typically a circular reference. */
[PersistentState]: Ref<T>;
/** The value the ref should be set to in a fresh save, or when updating an old save to the current version. */
@ -89,31 +92,65 @@ function getStackTrace() {
);
}
function checkNaNAndWrite<T extends State>(persistent: Persistent<T>, value: T) {
// Decimal is smart enough to return false on things that aren't supposed to be numbers
if (Decimal.isNaN(value as DecimalSource)) {
if (!state.hasNaN) {
player.autosave = false;
state.hasNaN = true;
state.NaNPath = persistent[SaveDataPath];
state.NaNPersistent = persistent as Persistent<DecimalSource>;
}
console.error(
`Attempted to save NaN value to`,
persistent[SaveDataPath]?.join("."),
persistent
);
throw "Attempted to set NaN value. See above for details";
}
persistent[PersistentState].value = value;
}
/**
* Create a persistent ref, which can be saved and loaded.
* All (non-deleted) persistent refs must be included somewhere within the layer object returned by that layer's options func.
* @param defaultValue The value the persistent ref should start at on fresh saves or when reset.
*/
export function persistent<T extends State>(defaultValue: T | Ref<T>): Persistent<T> {
const persistent = (
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
) as Persistent<T>;
const persistentState: Ref<T> = isRef(defaultValue)
? defaultValue
: (ref<T>(defaultValue) as Ref<T>);
persistent[PersistentState] = persistent;
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
persistent[StackTrace] = getStackTrace();
persistent[Deleted] = false;
const nonPersistent: Partial<NonPersistent<T>> = computed({
if (isRef(defaultValue)) {
defaultValue = defaultValue.value;
}
const nonPersistent = computed({
get() {
return persistent.value;
return persistentState.value;
},
set(value) {
persistent.value = value;
checkNaNAndWrite(persistent, value);
}
});
nonPersistent[DefaultValue] = persistent[DefaultValue];
persistent[NonPersistent] = nonPersistent as NonPersistent<T>;
persistent[SaveDataPath] = undefined;
}) as NonPersistent<T>;
nonPersistent[DefaultValue] = defaultValue;
// We're trying to mock a vue ref, which means the type expects a private [RefSymbol] property that we can't access, but the actual implementation of isRef just checks for `__v_isRef`
const persistent = {
get value() {
return persistentState.value as T;
},
set value(value: T) {
checkNaNAndWrite(persistent, value);
},
__v_isRef: true,
[PersistentState]: persistentState,
[DefaultValue]: defaultValue,
[StackTrace]: getStackTrace(),
[Deleted]: false,
[NonPersistent]: nonPersistent,
[SaveDataPath]: undefined
} as unknown as Persistent<T>;
if (addingLayers.length === 0) {
console.warn(
@ -125,7 +162,7 @@ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persisten
persistentRefs[addingLayers[addingLayers.length - 1]].add(persistent);
}
return persistent as Persistent<T>;
return persistent;
}
/**

View file

@ -1,13 +1,8 @@
import { isPlainObject } from "is-plain-object";
import Decimal from "util/bignum";
import type { ProxiedWithState } from "util/proxies";
import { ProxyPath, ProxyState } from "util/proxies";
import { reactive, unref } from "vue";
import type { Ref } from "vue";
import transientState from "./state";
import { reactive, unref } from "vue";
/** The player save data object. */
export interface PlayerData {
export interface Player {
/** The ID of this save. */
id: string;
/** A multiplier for time passing. Set to 0 when the game is paused. */
@ -36,9 +31,6 @@ export interface PlayerData {
layers: Record<string, LayerData<unknown>>;
}
/** The proxied player that is used to track NaN values. */
export type Player = ProxiedWithState<PlayerData>;
/** A layer's save data. Automatically unwraps refs. */
export type LayerData<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
@ -52,7 +44,7 @@ export type LayerData<T> = {
: T[P];
};
const state = reactive<PlayerData>({
const player = reactive<Player>({
id: "",
devSpeed: null,
name: "",
@ -68,90 +60,16 @@ const state = reactive<PlayerData>({
layers: {}
});
export default window.player = player;
/** Convert a player save data object into a JSON string. Unwraps refs. */
export function stringifySave(player: PlayerData): string {
export function stringifySave(player: Player): string {
return JSON.stringify(player, (key, value) => unref(value));
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const playerHandler: ProxyHandler<Record<PropertyKey, any>> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(target: Record<PropertyKey, any>, key: PropertyKey): any {
if (key === ProxyState || key === ProxyPath) {
return target[key];
}
const value = target[ProxyState][key];
if (key !== "value" && (isPlainObject(value) || Array.isArray(value))) {
if (value !== target[key]?.[ProxyState]) {
const path = [...target[ProxyPath], key];
target[key] = new Proxy({ [ProxyState]: value, [ProxyPath]: path }, playerHandler);
}
return target[key];
}
return value;
},
set(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
target: Record<PropertyKey, any>,
property: PropertyKey,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any,
receiver: ProxyConstructor
): boolean {
if (
!transientState.hasNaN &&
((typeof value === "number" && isNaN(value)) ||
(value instanceof Decimal &&
(isNaN(value.sign) || isNaN(value.layer) || isNaN(value.mag))))
) {
const currentValue = target[ProxyState][property];
if (
!(
(typeof currentValue === "number" && isNaN(currentValue)) ||
(currentValue instanceof Decimal &&
(isNaN(currentValue.sign) ||
isNaN(currentValue.layer) ||
isNaN(currentValue.mag)))
)
) {
state.autosave = false;
transientState.hasNaN = true;
transientState.NaNPath = [...target[ProxyPath], property];
transientState.NaNReceiver = receiver as unknown as Record<string, unknown>;
console.error(
`Attempted to set NaN value`,
[...target[ProxyPath], property],
target[ProxyState]
);
throw "Attempted to set NaN value. See above for details";
}
}
target[ProxyState][property] = value;
return true;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ownKeys(target: Record<PropertyKey, any>) {
return Reflect.ownKeys(target[ProxyState]);
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
has(target: Record<PropertyKey, any>, key: string) {
return Reflect.has(target[ProxyState], key);
},
getOwnPropertyDescriptor(target, key) {
return Object.getOwnPropertyDescriptor(target[ProxyState], key);
}
};
declare global {
/** Augment the window object so the player can be accessed from the console. */
interface Window {
player: Player;
}
}
/** The player save data object. */
export default window.player = new Proxy(
{ [ProxyState]: state, [ProxyPath]: ["player"] },
playerHandler
) as Player;

View file

@ -1,4 +1,6 @@
import type { DecimalSource } from "util/bignum";
import { shallowReactive } from "vue";
import type { Persistent } from "./persistence";
/** An object of global data that is not persistent. */
export interface Transient {
@ -8,8 +10,8 @@ export interface Transient {
hasNaN: boolean;
/** The location within the player save data object of the NaN value. */
NaNPath?: string[];
/** The parent object of the NaN value. */
NaNReceiver?: Record<string, unknown>;
/** The ref that was being set to NaN. */
NaNPersistent?: Persistent<DecimalSource>;
}
declare global {

View file

@ -1,6 +1,7 @@
import type { JSXFunction } from "features/feature";
import { isFunction } from "util/common";
import type { Ref } from "vue";
import { computed } from "vue";
import { isFunction } from "util/common";
export const DoNotCache = Symbol("DoNotCache");
@ -32,21 +33,22 @@ export function processComputable<T, S extends keyof ComputableKeysOf<T>>(
key: S
): asserts obj is T & { [K in S]: ProcessedComputable<UnwrapComputableType<T[S]>> } {
const computable = obj[key];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (isFunction(computable) && computable.length === 0 && !(computable as any)[DoNotCache]) {
if (
isFunction(computable) &&
computable.length === 0 &&
!(computable as unknown as JSXFunction)[DoNotCache]
) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
obj[key] = computed(computable.bind(obj));
} else if (isFunction(computable)) {
obj[key] = computable.bind(obj) as unknown as T[S];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(obj[key] as any)[DoNotCache] = true;
(obj[key] as unknown as JSXFunction)[DoNotCache] = true;
}
}
export function convertComputable<T>(obj: Computable<T>): ProcessedComputable<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (isFunction(obj) && !(obj as any)[DoNotCache]) {
if (isFunction(obj) && !(obj as unknown as JSXFunction)[DoNotCache]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
obj = computed(obj);

View file

@ -1,11 +1,11 @@
import type { Persistent } from "game/persistence";
import { NonPersistent } from "game/persistence";
import Decimal from "util/bignum";
export const ProxyState = Symbol("ProxyState");
export const ProxyPath = Symbol("ProxyPath");
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any>
export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, unknown>
? NonNullable<T> extends Decimal
? T
: {
@ -16,6 +16,18 @@ export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any
}
: T;
export type Proxied<T> = NonNullable<T> extends Record<PropertyKey, unknown>
? NonNullable<T> extends Persistent<infer S>
? NonPersistent<S>
: NonNullable<T> extends Decimal
? T
: {
[K in keyof T]: Proxied<T[K]>;
} & {
[ProxyState]: T;
}
: T;
// Takes a function that returns an object and pretends to be that object
// Note that the object is lazily calculated
export function createLazyProxy<T extends object, S extends T>(

View file

@ -1,13 +1,12 @@
import projInfo from "data/projInfo.json";
import { globalBus } from "game/events";
import type { Player, PlayerData } from "game/player";
import type { Player } from "game/player";
import player, { stringifySave } from "game/player";
import settings, { loadSettings } from "game/settings";
import LZString from "lz-string";
import { ProxyState } from "util/proxies";
import { ref } from "vue";
export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
export function setupInitialStore(player: Partial<Player> = {}): Player {
return Object.assign(
{
id: `${projInfo.id}-0`,
@ -27,11 +26,9 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
) as Player;
}
export function save(playerData?: PlayerData): string {
const stringifiedSave = LZString.compressToUTF16(
stringifySave(playerData ?? player[ProxyState])
);
localStorage.setItem((playerData ?? player[ProxyState]).id, stringifiedSave);
export function save(playerData?: Player): string {
const stringifiedSave = LZString.compressToUTF16(stringifySave(playerData ?? player));
localStorage.setItem((playerData ?? player).id, stringifiedSave);
return stringifiedSave;
}
@ -70,7 +67,7 @@ export async function load(): Promise<void> {
}
}
export function newSave(): PlayerData {
export function newSave(): Player {
const id = getUniqueID();
const player = setupInitialStore({ id });
save(player);
@ -91,7 +88,7 @@ export function getUniqueID(): string {
export const loadingSave = ref(false);
export async function loadSave(playerObj: Partial<PlayerData>): Promise<void> {
export async function loadSave(playerObj: Partial<Player>): Promise<void> {
console.info("Loading save", playerObj);
loadingSave.value = true;
const { layers, removeLayer, addLayer } = await import("game/layers");