Skip to content

Commit

Permalink
Update ClientSettings to support DataField and DataModel types
Browse files Browse the repository at this point in the history
  • Loading branch information
LukeAbby committed Oct 19, 2024
1 parent 050cce0 commit 0d49bd6
Show file tree
Hide file tree
Showing 12 changed files with 314 additions and 132 deletions.
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
// When array types get complex enough `Array<...>` is nicer looking than `(...)[]`.
"@typescript-eslint/array-type": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/consistent-indexed-object-style": "off",
// `allowSingleExtends` allows the pattern of `interface X extends _X {}`.
// This is done as a performance optimization or simply to display a different name in intellisense.
"@typescript-eslint/no-empty-interface": ["error", { allowSingleExtends: true }],
Expand Down
8 changes: 4 additions & 4 deletions src/foundry/client/apps/forms/combat-config.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ declare global {

protected override _updateObject(
event: Event,
formData: ClientSettings.Values["core.combatTrackerConfig"],
formData: SettingConfig["core.combatTrackerConfig"],
): Promise<unknown>;

override activateListeners(html: JQuery<HTMLElement>): void;
Expand All @@ -41,10 +41,10 @@ declare global {
type Any = CombatTrackerConfig<any>;

interface CombatTrackerConfigData extends FormApplication.FormApplicationData {
settings: ClientSettings.Values["core.combatTrackerConfig"];
settings: SettingConfig["core.combatTrackerConfig"];
attributeChoices: ReturnType<(typeof TokenDocument)["getTrackedAttributeChoices"]>;
combatTheme: SettingConfig<string>;
selectedTheme: ClientSettings.Values["core.combatTheme"];
combatTheme: SettingOptions<string>;
selectedTheme: SettingConfig["core.combatTheme"];
user: User;
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/foundry/client/config.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ declare global {
* }
* ```
*/
sounds: Record<string, CONFIG.Combat.SoundPreset>;
sounds: CONFIG.Combat.Sounds;
};

/**
Expand Down Expand Up @@ -2814,6 +2814,13 @@ declare global {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface RollModes extends Record<foundry.CONST.DICE_ROLL_MODES, string> {}
}

namespace Combat {
interface Sounds {
epic: CONFIG.Combat.SoundPreset;
mc: CONFIG.Combat.SoundPreset;
}
}
}

const CONFIG: CONFIG;
Expand Down
127 changes: 105 additions & 22 deletions src/foundry/client/core/settings.d.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { ConformRecord } from "../../../types/helperTypes.d.mts";
import type { AnyArray, AnyObject, InexactPartial } from "../../../types/utils.d.mts";
import type DataModel from "../../common/abstract/data.d.mts";

Check warning on line 3 in src/foundry/client/core/settings.d.mts

View workflow job for this annotation

GitHub Actions / lint code base

Using exported name 'DataModel' as identifier for default export
import type Document from "../../common/abstract/document.d.mts";
import type { DataField } from "../../common/data/fields.d.mts";

declare global {
/**
Expand Down Expand Up @@ -42,11 +45,8 @@ declare global {
* Register a new game setting under this setting scope
*
* @param namespace - The namespace under which the setting is registered
* @param key - The key name for the setting under the namespace module
* @param key - The key name for the setting under the namespace
* @param data - Configuration for setting data
* @typeParam N - The namespace under which the setting is registered, as a type
* @typeParam K - The key name for the setting under the namespace module, as a type
* @typeParam T - The type of the setting value
*
* @example Register a client setting
* ```typescript
Expand All @@ -55,6 +55,7 @@ declare global {
* hint: "A description of the registered setting and its behavior.",
* scope: "client", // This specifies a client-stored setting
* config: true, // This specifies that the setting appears in the configuration view
* requiresReload: true // This will prompt the user to reload the application for the setting to take effect.
* type: String,
* choices: { // If choices are defined, the resulting setting will be a select menu
* "a": "Option A",
Expand All @@ -74,25 +75,22 @@ declare global {
* hint: "A description of the registered setting and its behavior.",
* scope: "world", // This specifies a world-level setting
* config: true, // This specifies that the setting appears in the configuration view
* type: Number,
* range: { // If range is specified, the resulting setting will be a range slider
* min: 0,
* max: 100,
* step: 10
* }
* requiresReload: true // This will prompt the GM to have all clients reload the application for the setting to
* // take effect.
* type: new foundry.fields.NumberField({nullable: false, min: 0, max: 100, step: 10}),
* default: 50, // The default value for the setting
* onChange: value => { // A callback function which triggers when the setting is changed
* console.log(value)
* }
* });
* ```
*/
register<N extends SettingsConfig.Namespace, K extends SettingsConfig.Key, T>(
register<N extends ClientSettings.Namespace, K extends ClientSettings.Key, T extends ClientSettings.Type>(
namespace: N,
key: K,
data: SettingsConfig[`${N}.${K}`] extends string | number | boolean | AnyArray | AnyObject | null
? ClientSettings.RegisterSetting<SettingsConfig[`${N}.${K}`]>
: ClientSettings.RegisterSetting<T>,
data: ClientSettings.Type extends T
? ClientSettings.RegisterSetting<_SettingConfig[`${N}.${K}`]>
: ClientSettings.RegisterSetting<NoInfer<T>>,
): void;

/**
Expand Down Expand Up @@ -131,10 +129,10 @@ declare global {
* game.settings.get("myModule", "myClientSetting");
* ```
*/
get<N extends SettingsConfig.Namespace, K extends SettingsConfig.Key>(
get<N extends ClientSettings.Namespace, K extends ClientSettings.Key>(
namespace: N,
key: K,
): SettingsConfig[`${N}.${K}`];
): ClientSettings.SettingInitializedType<N, K>;

/**
* Set the value of a game setting for a certain namespace and setting key
Expand All @@ -154,23 +152,108 @@ declare global {
* game.settings.set("myModule", "myClientSetting", "b");
* ```
*/
set<N extends SettingsConfig.Namespace, K extends SettingsConfig.Key>(
set<N extends ClientSettings.Namespace, K extends ClientSettings.Key>(
namespace: N,
key: K,
value: SettingsConfig[`${N}.${K}`],
value: ClientSettings.SettingAssignmentType<N, K>,
options?: Document.ModificationContext<Document.Any | null>,
): Promise<SettingsConfig[`${N}.${K}`]>;
): Promise<ClientSettings.SettingInitializedType<N, K>>;
}

namespace ClientSettings {
type RegisterSetting<T = unknown> = InexactPartial<Omit<SettingConfig<T>, "key" | "namespace">>;
type Namespace = GetNamespaces<keyof _SettingConfig>;
type Key = GetKeys<keyof _SettingConfig>;

/**
* A compile type is a type for a setting that only exists at compile time.
* For example `string` does not correspond to a real runtime value like `String` does.
*/
type TypeScriptType = string | number | boolean | symbol | bigint | AnyArray | AnyObject;
type RuntimeType = DataField.Any | DataModel.Any | SettingFunction | SettingConstructor;

type Type = TypeScriptType | RuntimeType;

type ToRuntimeType<T extends Type> =
| (T extends RuntimeType ? T : never)
| (T extends string ? typeof String : never)
| (T extends number ? typeof Number : never)
| (T extends boolean ? typeof Boolean : never)
| (T extends symbol ? typeof Symbol : never)
| (T extends bigint ? typeof BigInt : never)
| (T extends readonly (infer V)[] ? typeof Array<V> : never)
| (T extends AnyObject ? typeof Object : never);

type SettingAssignmentType<N extends Namespace, K extends Key> = ToSettingAssignmentType<ConfiguredType<N, K>>;
type ToSettingAssignmentType<T extends Type> = ReplaceUndefinedWithNull<
| SettingType<T>
// TODO(LukeAbby): The `fromSource` function is called with `strict` which changes how fallback behaviour works. See `ClientSettings#set`
| (T extends DataModel.AnyConstructor ? DataModel.ConstructorDataFor<InstanceType<T>> : never)
>;

type SettingInitializedType<N extends Namespace, K extends Key> = ToSettingInitializedType<ConfiguredType<N, K>>;
type ToSettingInitializedType<T extends Type> = ReplaceUndefinedWithNull<
SettingType<T> | (T extends DataModel.Any ? T : never)
>;

type RegisterSetting<T extends Type = (value: unknown) => unknown> = InexactPartial<
Omit<SettingOptions<T>, "key" | "namespace">
>;

type RegisterSubmenu = Omit<SettingSubmenuConfig, "key" | "namespace">;

/**
* @deprecated - {@link SettingsConfig | `SettingsConfig`}
* @deprecated - {@link SettingConfig | `SettingConfig`}
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface Values extends SettingsConfig {}
interface Values extends SettingConfig {}
}
}

type SettingFunction = (value: never) => unknown;
type SettingConstructor = new (value: never) => unknown;

// Matches what's in `Setting#PRIMITIVE_TYPES`.
// Foundry itself uses this nomenclature despite the fact that it's a misnomer.
type PRIMITIVE_TYPES = readonly [
typeof String,
typeof Number,
typeof Boolean,
typeof Array,
typeof Symbol,
typeof BigInt,
];

// A type can be both a constructor and a function at once.
// Foundry prioritizes the constructor over the function unless it's in `Setting#PRIMITIVE_TYPES`.
type ConstructorToSettingType<T extends SettingConstructor> = T extends PRIMITIVE_TYPES[number]
? PrimitiveConstructorToSettingType<T>
: InstanceType<T>;

// In theory this is just the `ReturnType<T>`.
// However the function end of `Array` returns `any[]` while `Object` returns `any`.
// To increase safety they're special cased here.
type PrimitiveConstructorToSettingType<T extends PRIMITIVE_TYPES[number]> = T extends typeof Array
? AnyArray
: T extends typeof Object
? AnyObject
: ReturnType<T>;

type ConfiguredType<N extends ClientSettings.Namespace, K extends ClientSettings.Key> = _SettingConfig[`${N}.${K}`];

type SettingType<T extends ClientSettings.Type> =
// Note(LukeAbby): This isn't written as `T extends ClientSettings.TypeScriptType ? T : never` because then types like `DataField.Any` would be matched.
| (T extends ClientSettings.RuntimeType ? never : T)
// TODO(LukeAbby): The `validate` function is called with `strict` which changes how fallback behaviour works. See `ClientSettings#set`
| (T extends DataField.Any ? DataField.AssignmentTypeFor<T> : never)
| (T extends SettingConstructor ? ConstructorToSettingType<T> : T extends SettingFunction ? ReturnType<T> : never);

type ReplaceUndefinedWithNull<T> = T extends undefined ? null : T;

type GetNamespaces<SettingPath extends string> = SettingPath extends `${infer Scope}.${string}` ? Scope : never;
type GetKeys<SettingPath extends string> = SettingPath extends `${string}.${infer Name}` ? Name : never;

type _SettingConfig = ConformRecord<
// Refers to the deprecated interface so that merging works both ways.
ClientSettings.Values,
ClientSettings.Type
>;
2 changes: 1 addition & 1 deletion src/foundry/client/data/collections/combats.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ declare global {
/**
* Provide the settings object which configures the Combat document
*/
static get settings(): ClientSettings.Values[`core.${(typeof Combat)["CONFIG_SETTING"]}`];
static get settings(): SettingConfig[`core.${(typeof Combat)["CONFIG_SETTING"]}`];

override get directory(): (typeof ui)["combat"];

Expand Down
2 changes: 1 addition & 1 deletion src/foundry/client/data/documents/combat.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ declare global {

/** Return the object of settings which modify the Combat Tracker behavior */
// Type is copied here to avoid recursion issue
get settings(): ClientSettings.Values[`core.${(typeof Combat)["CONFIG_SETTING"]}`];
get settings(): SettingConfig[`core.${(typeof Combat)["CONFIG_SETTING"]}`];

/** Has this combat encounter been started? */
get started(): boolean;
Expand Down
14 changes: 13 additions & 1 deletion src/foundry/common/abstract/data.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ declare abstract class DataModel<
*/
// TODO(LukeAbby): Make only optional if `{}` is assignable to `InnerAssignmentType`.
constructor(
data?: fields.SchemaField.InnerAssignmentType<Schema> | DataModel<Schema, any>,
data?: DataModel.ConstructorData<Schema>,
{ parent, strict, ...options }?: DataModel.ConstructorOptions<Parent>,
);

Expand Down Expand Up @@ -395,6 +395,12 @@ declare abstract class DataModel<
}

declare namespace DataModel {
type ConstructorData<Schema extends DataSchema> =
| fields.SchemaField.InnerAssignmentType<Schema>
| DataModel<Schema, any>;

type ConstructorDataFor<ConcreteDataModel extends DataModel.Any> = ConstructorData<SchemaFor<ConcreteDataModel>>;

interface ConstructorOptions<Parent extends Any | null = null> {
/**
* A parent DataModel instance to which this DataModel belongs
Expand All @@ -417,6 +423,8 @@ declare namespace DataModel {

type Any = DataModel<DataSchema, any>;

type AnyConstructor = typeof AnyDataModel;

/**
* A helper type to extract the {@link SchemaFor} from a {@link DataModel}.
* @typeParam ModelType - the DataModel for the embedded data
Expand All @@ -435,5 +443,9 @@ declare namespace DataModel {
}
}

declare class AnyDataModel extends DataModel<any, any> {
constructor(arg0: never, ...args: never[]);
}

// Matches foundry exporting class as both default and non-default
export { DataModel };
Loading

0 comments on commit 0d49bd6

Please sign in to comment.