Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flag interfaces #7

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 70 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,89 @@ start({}, () => {
}
});

for (const [entity, position] of query<[Vector3, Without<[CFrame]>]>()) {
print(`Entity: ${entity}, Position: ${position}`);
interface Person extends Flag {}
interface Location extends Flag {}
interface Owns extends Flag {}

interface Ketchup extends Flag {}
interface Organic extends Flag {}
interface Expired extends Flag {}

interface Stats {
spiciness: number;
tasteRating: number;
}

interface Quantity {
amount: number;
}

// Example of using pair relationships between entities
interface Likes {}
interface Eats {
count: number;
interface Rates {
comment: string;
score: number;
}

interface Fruit {}
const frontShelf = spawn<[Location]>();
const topShelf = spawn<[Location]>();
const backOfFridge = spawn<[Location]>();

const alice = spawn();
const bob = spawn();
const charlie = spawn();
const marcus = spawn<[Person]>();

const banana = spawn();
add<Fruit>(banana);
const felix = spawn<[Stats, Quantity, Ketchup]>([{ spiciness: 2, tasteRating: 7 }, { amount: 5 }]);
add<Pair<ChildOf>>(felix, frontShelf);

add<Pair<Likes>>(alice, bob);
add<Pair<Likes>>(alice, charlie);
const organicKetchup = spawn<[Stats, Quantity, Ketchup, Organic]>([
{ spiciness: 3, tasteRating: 8 },
{ amount: 2 },
]);
add<Pair<ChildOf>>(organicKetchup, topShelf);

add<Pair<Likes, Fruit>>(bob);
const oldKetchup = spawn<[Stats, Quantity, Ketchup, Expired]>([
{ spiciness: 7, tasteRating: 1 },
{ amount: 1 },
]);
add<Pair<ChildOf>>(oldKetchup, backOfFridge);

set<Pair<Eats>>(bob, banana, { count: 5 });
set<Pair<Eats, Fruit>>(alice, { count: 12 });
add<Pair<Owns>>(marcus, felix);
set<Pair<Rates>>(marcus, felix, {
comment: "The superior ketchup for.. hotdog mashed potatoes?!!",
score: 10,
});

for (const [entity] of query().pair<Likes>(alice)) {
const likedEntity = target<Likes>(entity);
print(`Entity ${entity} likes ${likedEntity}`);
add<Pair<Owns>>(marcus, organicKetchup);
set<Pair<Rates>>(marcus, organicKetchup, {
comment: "Fancy, but still not Felix",
score: 7,
});

add<Pair<Owns>>(marcus, oldKetchup);

// Find all of Marcus's ketchup that isn't expired
for (const [entity, stats, quantity] of query<
[Stats, Quantity, With<[Ketchup, Pair<ChildOf>]>, Without<Expired>]
>()) {
if (has<Pair<Owns>>(marcus, entity)) {
const rating = get<Pair<Rates>>(marcus, entity);
const location = parent(entity);
print(`Marcus owns this ketchup at entity ${location}:`);
print(`- Amount left: ${quantity.amount} bottles`);
print(`- Taste rating: ${stats.tasteRating}`);
print(`- Marcus's review: ${rating?.comment ?? "Not rated yet"}`);
}
}

for (const [entity, eatsData] of query<[Pair<Eats, Fruit>]>()) {
const eatsTarget = target<Eats>(entity);
print(`Entity ${entity} eats ${eatsData.count} fruit (${eatsTarget})`);
// Find all expired ketchup and their locations
for (const [entity, quantity] of query<[Quantity, With<[Ketchup, Expired, Pair<ChildOf>]>]>()) {
const location = parent(entity);
print(`Warning: Found ${quantity.amount} expired ketchup bottles at entity ${location}!`);

set<Quantity>(entity, { amount: 0 });
}

// Using Pair<P> to match any target (wildcard), equivalent to Pair<Likes, Wildcard>
for (const [entity] of query<[Pair<Likes>]>()) {
const likedTarget = target<Likes>(entity);
print(`Entity ${entity} likes ${likedTarget}`);
// Despawn any ketchup that has no bottles left
for (const [entity, quantity] of query<[Quantity, With<Ketchup>]>()) {
if (quantity.amount === 0) {
despawn(entity);
}
}
```
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from "./hooks/use-event";
export * from "./hooks/use-throttle";
export * from "./query";
export type { ChildOf, Entity, Id, Pair, Tag, Wildcard } from "./registry";
export type { ChildOf, Entity, Flag, Id, Pair, Tag, Wildcard } from "./registry";
export {
add,
added,
Expand Down
24 changes: 17 additions & 7 deletions src/query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type { Modding } from "@flamework/core";
import * as ecs from "@rbxts/jecs";

import type { Entity, FilterPairs, Id, ResolveKeys } from "./registry";
import type {
ComponentKey,
Entity,
FilterPairs,
Id,
OmitFlag,
OmitFlags,
ResolveKeys,
} from "./registry";
import { component, getId, registry } from "./registry";

// Almost full credits to @fireboltofdeath for all of these types.
Expand Down Expand Up @@ -37,7 +44,10 @@ type Calculate<T extends Array<unknown>, B extends Bounds = Bounds> = T extends
: Calculate<Skip<T>, PushBound<B, "query", T[0]>>;

type ToIds<T> = T extends [] ? undefined : ResolveKeys<T>;
type ExtractQueryTypes<T extends Array<unknown>> = Reconstruct<FilterPairs<Calculate<T>["query"]>>;

type ExtractQueryTypes<T extends Array<unknown>> = Reconstruct<
OmitFlags<FilterPairs<Calculate<T>["query"]>>
>;

type QueryHandle<T extends Array<unknown>> = {
__iter(): IterableFunction<LuaTuple<[Entity, ...T]>>;
Expand All @@ -53,19 +63,19 @@ type QueryHandle<T extends Array<unknown>> = {
* @returns A new QueryHandle with the pair filter added.
* @metadata macro
*/
pair<P>(object: Entity, predicate?: Modding.Generic<P, "id">): QueryHandle<[...T, P]>;
pair<P>(object: Entity, predicate?: ComponentKey<P>): QueryHandle<[...T, OmitFlag<P>]>;
terms?: Array<Id>;
} & IterableFunction<LuaTuple<[Entity, ...T]>>;

function queryPair<T extends Array<unknown>, P>(
this: QueryHandle<T>,
object: Entity,
predicate?: Modding.Generic<P, "id">,
): QueryHandle<[...T, P]> {
predicate?: ComponentKey<P>,
): QueryHandle<[...T, OmitFlag<P>]> {
assert(predicate);
const id = ecs.pair(component(predicate), object);
this.terms = this.terms ? [...this.terms, id] : [id];
return this as unknown as QueryHandle<[...T, P]>;
return this as unknown as QueryHandle<[...T, OmitFlag<P>]>;
}

function queryIter<T extends Array<unknown>>(
Expand Down
69 changes: 48 additions & 21 deletions src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import * as ecs from "@rbxts/jecs";

import { createSignal, type Signal } from "./signal";

export interface Wildcard {}
export interface ChildOf {}
export interface Flag {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change the name of this interface and every subsequent dependents on this interface to use "Tag"

Copy link
Collaborator Author

@hautajoki hautajoki Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag is already used for Entity<.undefined>. Should I just get rid of that then?

Copy link
Collaborator

@Ukendio Ukendio Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I just get rid of that then?

Yeah, that sounds reasonable.

readonly __flamecs_flag: unique symbol;
}

export interface Wildcard extends Flag {}
export interface ChildOf extends Flag {}

export type Entity<T = unknown> = ecs.Entity<T>;
export type Id<T = unknown> = ecs.Id<T>;
Expand All @@ -27,6 +31,15 @@ export type FilterPairs<T> = {
[K in keyof T]: FilterPair<T[K]>;
};

export type OmitFlag<T> = T extends Flag ? undefined : T;
export type OmitFlags<T> = {
[K in keyof T]: OmitFlag<T[K]>;
};

type WithTrailingUndefined<T extends Array<unknown>> = T extends [...infer Rest, undefined]
? [...WithTrailingUndefined<Rest>, undefined?]
: T;

const components = new Map<string, Entity>();
export const registry = new ecs.World();

Expand Down Expand Up @@ -75,7 +88,7 @@ function hookListeners<T>(id: Entity<T>): void {
* @param key - Flamework autogenerated key.
* @metadata macro
*/
export function reserve<T>(runtimeId: Entity<T>, key?: Modding.Generic<T, "id">): void {
export function reserve<T>(runtimeId: Entity<T>, key?: ComponentKey<T>): void {
assert(key);
assert(!components.has(key), `A component with the key "${key}" already exists`);
components.set(key, runtimeId);
Expand All @@ -94,12 +107,12 @@ export function reserve<T>(runtimeId: Entity<T>, key?: Modding.Generic<T, "id">)
* @returns The component entity ID.
* @metadata macro
*/
export function component<T>(key?: ComponentKey<T>): Entity<T> {
export function component<T>(key?: ComponentKey<T>): Entity<OmitFlag<T>> {
assert(key);
let id = components.get(key) as Entity<T> | undefined;
let id = components.get(key) as Entity<OmitFlag<T>> | undefined;

if (id === undefined) {
id = registry.component<T>();
id = registry.component<OmitFlag<T>>();
components.set(key, id);
hookListeners(id);
}
Expand All @@ -115,7 +128,7 @@ export function component<T>(key?: ComponentKey<T>): Entity<T> {
* @returns The component or pair ID.
* @metadata macro.
*/
export function getId<T>(key?: ResolveKey<T>): Id<FilterPair<T>> {
export function getId<T>(key?: ResolveKey<T>): Id<OmitFlag<FilterPair<T>>> {
assert(key);

if (typeIs(key, "table")) {
Expand All @@ -139,15 +152,20 @@ export function getId<T>(key?: ResolveKey<T>): Id<FilterPair<T>> {
* @metadata macro
*/
export function spawn<T extends Array<unknown>>(
bundle?: FilterPairs<T>,
bundle?: WithTrailingUndefined<OmitFlags<FilterPairs<T>>>,
keys?: ResolveKeys<T>,
): Tag {
const entity = registry.entity();

if (bundle && keys) {
if (keys) {
for (let index = 0; index < keys.size(); index++) {
const id = getId(keys[index]);
registry.set(entity, id, bundle[index]);
const value = bundle?.[index];
if (value !== undefined) {
registry.set(entity, id, value);
} else {
registry.add(entity, id);
}
}
}

Expand All @@ -174,13 +192,19 @@ export function despawn(entity: Entity): void {
*/
export function insert<T extends Array<unknown>>(
entity: Entity,
values: FilterPairs<T>,
values?: WithTrailingUndefined<OmitFlags<FilterPairs<T>>>,
keys?: ResolveKeys<T>,
): void {
assert(keys);

for (let index = 0; index < keys.size(); index++) {
const id = getId(keys[index]);
registry.set(entity, id, values[index]);
const value = values?.[index];
if (value !== undefined) {
registry.set(entity, id, value);
} else {
registry.add(entity, id);
}
}
}

Expand All @@ -193,7 +217,7 @@ export function insert<T extends Array<unknown>>(
* @param key - Flamework autogenerated key.
* @metadata macro
*/
export function add<T extends Pair<defined>>(
export function add<T extends Pair<defined & Flag>>(
entity: Entity,
object: Entity,
key?: ComponentKey<FilterPair<T>>,
Expand All @@ -208,7 +232,10 @@ export function add<T extends Pair<defined>>(
* @param key - Flamework autogenerated key.
* @metadata macro
*/
export function add<T extends Pair<defined, defined>>(entity: Entity, key?: ResolveKey<T>): void;
export function add<T extends Pair<defined & Flag, defined>>(
entity: Entity,
key?: ResolveKey<T>,
): void;

/**
* Adds a component to an entity.
Expand All @@ -218,7 +245,7 @@ export function add<T extends Pair<defined, defined>>(entity: Entity, key?: Reso
* @param key - Flamework autogenerated key.
* @metadata macro
*/
export function add<T>(entity: Entity, key?: ComponentKey<T>): void;
export function add<T extends Flag>(entity: Entity, key?: ComponentKey<T>): void;

export function add(entity: Entity, argument1?: unknown, argument2?: unknown): void {
if (argument2 !== undefined) {
Expand Down Expand Up @@ -295,7 +322,7 @@ export function remove(entity: Entity, argument1?: unknown, argument2?: unknown)
export function set<T extends Pair<defined>>(
entity: Entity,
object: Entity,
value: FilterPair<T>,
value: OmitFlag<FilterPair<T>>,
key?: ComponentKey<FilterPair<T>>,
): void;

Expand All @@ -311,7 +338,7 @@ export function set<T extends Pair<defined>>(
*/
export function set<T extends Pair<defined, defined>>(
entity: Entity,
value: FilterPair<T>,
value: OmitFlag<FilterPair<T>>,
key?: ResolveKey<T>,
): void;

Expand All @@ -324,7 +351,7 @@ export function set<T extends Pair<defined, defined>>(
* @param key - Flamework autogenerated key.
* @metadata macro
*/
export function set<T>(entity: Entity, value: T, key?: ComponentKey<T>): void;
export function set<T>(entity: Entity, value: OmitFlag<T>, key?: ComponentKey<T>): void;

export function set(
entity: Entity,
Expand Down Expand Up @@ -360,7 +387,7 @@ export function get<T extends Pair<defined>>(
entity: Entity,
object: Entity,
key?: ComponentKey<FilterPair<T>>,
): FilterPair<T> | undefined;
): OmitFlag<FilterPair<T>> | undefined;

/**
* Retrieves the value of a component or pair for a specific entity.
Expand All @@ -371,7 +398,7 @@ export function get<T extends Pair<defined>>(
* @returns The value associated with the component or pair.
* @metadata macro
*/
export function get<T>(entity: Entity, key?: ResolveKey<T>): FilterPair<T> | undefined;
export function get<T>(entity: Entity, key?: ResolveKey<T>): OmitFlag<FilterPair<T>> | undefined;

export function get(entity: Entity, argument1?: unknown, argument2?: unknown): unknown {
if (argument2 !== undefined) {
Expand Down Expand Up @@ -462,7 +489,7 @@ export function parent(entity: Entity): Entity | undefined {
* @returns The pair ID.
* @metadata macro
*/
export function pair<P>(object: Entity, predicate?: ComponentKey<P>): Pair<P, unknown> {
export function pair<P>(object: Entity, predicate?: ComponentKey<P>): Pair<OmitFlag<P>, unknown> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just saw thiis but the first element is the predicate not the object.

Copy link
Collaborator Author

@hautajoki hautajoki Nov 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The predicate here is a flamework generated key. pair<SomeComponent>(object)

const predicateId = component(predicate);
return ecs.pair(predicateId, object);
}
Expand Down