Skip to content

Full pair syntax support #5

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

Merged
merged 13 commits into from
Oct 14, 2024
Merged
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
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
/node_modules
/out
/include
/dist

*.tsbuildinfo
flamework.build
sourcemap.json

/*.rbxlx.lock
/*.rbxl.lock
**/*.rbx[lm]*

.env

.DS_Store
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"tabWidth": 4,
"trailingComma": "all",
"useTabs": true
}
}
59 changes: 31 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,57 @@ Flamework + ECS = Flamecs 🔥
- Component Metadata (soon)

```ts
const e = spawn<[Vector3]>([new Vector3()]);
print(has<Vector3>(e));
const positionEntity = spawn<[Vector3]>([new Vector3(10, 20, 30)]);
print(has<Vector3>(positionEntity));

start({}, () => {
if (useThrottle(5)) {
for (const [entity, vec3, cf] of query<[Vector3, CFrame]>()) {
print(entity, vec3, cf);
for (const [entity, position, orientation] of query<[Vector3, CFrame]>()) {
print(`Entity: ${entity}, Position: ${position}, Orientation: ${orientation}`);
}
}
});

for (const [e, vec] of query<[Vector3, Without<[CFrame]>]>()) {
print(e, vec);
for (const [entity, position] of query<[Vector3, Without<[CFrame]>]>()) {
print(`Entity: ${entity}, Position: ${position}`);
}

// Example of using pairs
// Example of using pair relationships between entities
interface Likes {}
interface Eats {}
interface Apple {}
interface Eats {
count: number;
}

interface Fruit {}

const alice = spawn();
const bob = spawn();
const charlie = spawn();

registry.add(alice, pair<Likes>(bob));
registry.add(alice, pair<Likes>(charlie));
const banana = spawn();
add<Fruit>(banana);

add<Pair<Likes>>(alice, bob);
add<Pair<Likes>>(alice, charlie);

add<Pair<Likes, Fruit>>(bob);

// The type of a pair is determined by its first component in the relationship.
// In this case, 'Eats' is defined as an empty interface {}.
// An empty interface is a structural type that matches any object.
// This allows us to set Pair<Eats, Apple> to 3, though it's not recommended
// as it effectively becomes an 'any' type, losing type safety.
set<Pair<Eats, Apple>>(bob, 3);
set<Pair<Eats>>(bob, banana, { count: 5 });
set<Pair<Eats, Fruit>>(alice, { count: 12 });

for (const [e] of query().pair<Likes>(bob)) {
const liked = target<Likes>(e);
print(`${e} likes ${liked}`);
for (const [entity] of query().pair<Likes>(alice)) {
const likedEntity = target<Likes>(entity);
print(`Entity ${entity} likes ${likedEntity}`);
}

for (const [e, amount] of query<[Pair<Eats, Apple>]>()) {
const eatsTarget = target<Eats>(e);
print(`${e} eats ${amount} ${eatsTarget}`);
for (const [entity, eatsData] of query<[Pair<Eats, Fruit>]>()) {
const eatsTarget = target<Eats>(entity);
print(`Entity ${entity} eats ${eatsData.count} fruit (${eatsTarget})`);
}

// Using Pair<P> to match any target (wildcard)
// equivelant to Pair<Likes, Wildcard>
for (const [e] of query<[Pair<Likes>]>()) {
const likedTarget = target<Likes>(e);
print(`${e} likes ${likedTarget}`);
// 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}`);
}
```
12 changes: 6 additions & 6 deletions default.project.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flamecs",
"tree": {
"$path": "out"
}
}
{
"name": "flamecs",
"tree": {
"$path": "out"
}
}
7 changes: 1 addition & 6 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ export default style(
"max-lines-per-function": "off",
"no-param-reassign": "off",
"sonar/cognitive-complexity": "off",
"ts/no-empty-object-type": [
"error",
{
allowInterfaces: "always",
},
],
"ts/no-empty-object-type": "off",
"ts/no-non-null-assertion": "off",
},
typescript: {
Expand Down
10 changes: 0 additions & 10 deletions flamework.build

This file was deleted.

16 changes: 6 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"dependencies": {
"@flamework/core": "^1.2.3",
"@rbxts/flamework-binary-serializer": "^0.6.0",
"@rbxts/jecs": "github:Ukendio/jecs",
"rbxts-transformer-flamework": "^1.2.3"
},
"devDependencies": {
Expand Down
94 changes: 49 additions & 45 deletions src/hooks/use-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,74 @@ import type { Modding } from "@flamework/core";

import { useHookState } from "../topo";

type DisconnectFunction = () => void;

interface Connection {
Disconnect?(this: Connection): void;
disconnect?(this: Connection): void;
}

type Callback<T extends Array<unknown>> = (...args: T) => void;

type Signal<T extends Array<unknown>> =
| ((callback: Callback<T>) => Connection)
| {
connect?(this: Signal<T>, callback: Callback<T>): Connection;
Connect?(this: Signal<T>, callback: Callback<T>): Connection;
on?(this: Signal<T>, callback: Callback<T>): Connection;
};
type EventLike<T extends Array<unknown> = Array<unknown>> =
| ((callback: Callback<T>) => ConnectionLike)
| { Connect(callback: Callback<T>): ConnectionLike }
| { connect(callback: Callback<T>): ConnectionLike }
| { on(callback: Callback<T>): ConnectionLike };

type ConnectionLike = (() => void) | { Disconnect(): void } | { disconnect(): void };

interface EventStorage<T extends Array<unknown>> {
connection: Connection | DisconnectFunction | undefined;
connection?: ConnectionLike;
events: Array<T>;
}

function connect<T extends Array<unknown>>(
event: EventLike<T>,
callback: Callback<T>,
): ConnectionLike {
if (typeIs(event, "function")) {
return event(callback);
} else if (typeIs(event, "RBXScriptSignal") || "Connect" in event) {
return event.Connect(callback);
} else if ("connect" in event) {
return event.connect(callback);
} else if ("on" in event) {
return event.on(callback);
}

throw "Event-like object does not have a supported connect method.";
}

function disconnect(connection: ConnectionLike): void {
if (typeIs(connection, "function")) {
connection();
} else if (typeIs(connection, "RBXScriptConnection") || "Disconnect" in connection) {
connection.Disconnect();
} else if ("disconnect" in connection) {
connection.disconnect();
} else {
throw "Connection-like object does not have a supported disconnect method.";
}
}

function cleanup<T extends Array<unknown>>(storage: EventStorage<T>): boolean {
if (storage.connection) {
if (typeIs(storage.connection, "function")) {
storage.connection();
} else if (storage.connection.disconnect !== undefined) {
storage.connection.disconnect();
} else if (storage.connection.Disconnect !== undefined) {
storage.connection.Disconnect();
} else {
warn("No disconnect method found on the connection object.");
}
disconnect(storage.connection);
}

return true;
}

/**
* A hook for easy event listening with context awareness.
* Utility for handling event-like objects.
*
* Connects to the provided event-like object and stores incoming events.
* Returns an iterable function that yields the stored events in the order they
* were received.
*
* @template T - The type of the event arguments.
* @param event - The signal to listen to.
* @param discriminator - A unique value to additionally key by.
* @template T - The tuple type of event arguments.
* @param event - The event-like object to connect to.
* @param discriminator - An optional value to additionally key by.
* @param key - An automatically generated key to store the event state.
* @returns An iterable function that yields event arguments.
* @returns An iterable function that yields stored events.
* @metadata macro
*/
export function useEvent<T extends Array<unknown>>(
event: Signal<T>,
event: EventLike<T>,
discriminator?: unknown,
key?: Modding.Caller<"uuid">,
): IterableFunction<T> {
Expand All @@ -61,21 +79,7 @@ export function useEvent<T extends Array<unknown>>(

if (!storage.connection) {
storage.events = [];
const eventCallback: Callback<T> = (...args) => {
storage.events.push(args);
};

if (typeIs(event, "function")) {
storage.connection = event(eventCallback);
} else if (event.connect !== undefined) {
storage.connection = event.connect(eventCallback);
} else if (event.Connect !== undefined) {
storage.connection = event.Connect(eventCallback);
} else if (event.on !== undefined) {
storage.connection = event.on(eventCallback);
} else {
error("No connect method found on the event object.");
}
storage.connection = connect(event, (...args) => storage.events.push(args));
}

return (() => storage.events.shift()) as IterableFunction<T>;
Expand Down
7 changes: 3 additions & 4 deletions src/hooks/use-throttle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useHookState } from "../topo";

interface ThrottleStorage {
expiry: number;
time: number;
time?: number;
}

function cleanup(storage: ThrottleStorage): boolean {
Expand All @@ -18,7 +18,7 @@ function cleanup(storage: ThrottleStorage): boolean {
* time this function returned `true`. Always returns `true` the first time.
*
* @param seconds - The number of seconds to throttle for.
* @param discriminator - A unique value to additionally key by.
* @param discriminator - An optional value to additionally key by.
* @param key - An automatically generated key to store the throttle state.
* @returns - Returns true every x seconds, otherwise false.
* @metadata macro
Expand All @@ -29,11 +29,10 @@ export function useThrottle(
key?: Modding.Caller<"uuid">,
): boolean {
assert(key);

const storage = useHookState<ThrottleStorage>(key, discriminator, cleanup);

const currentTime = os.clock();
if (currentTime - storage.time >= seconds) {
if (storage.time === undefined || currentTime - storage.time >= seconds) {
storage.time = currentTime;
storage.expiry = currentTime + seconds;
return true;
Expand Down
Loading