diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..826b210 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.* text eol=lf \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..a302509 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,38 @@ +name: CI + +on: + pull_request: + branches: + - master + workflow_dispatch: + +permissions: + contents: write + packages: write + +env: + CI: true + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] + + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + + - name: Install npm + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.vscode/extensions.json b/.vscode/extensions.json index c544019..bc4c283 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,4 +3,4 @@ "roblox-ts.vscode-roblox-ts", "dbaeumer.vscode-eslint" ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 98eba36..6c6dab8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,12 +19,11 @@ { "rule": "*semi", "severity": "off", "fixable": true } ], // Enable eslint for all supported languages - "eslint.validate": ["typescript", "typescriptreact", "markdown", "json", "jsonc", "yaml", "toml", "luau"], + "eslint.validate": ["typescript", "typescriptreact", "json", "jsonc", "yaml", "toml", "luau"], // Auto fix "editor.codeActionsOnSave": { "source.fixAll.eslint": "always", "source.organizeImports": "never" }, - "eslint.options": { "flags": ["unstable_ts_config"] }, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "eslint.options": { "flags": ["unstable_ts_config"] } } diff --git a/archetypeTree.luau b/archetypeTree.luau deleted file mode 100644 index 192d758..0000000 --- a/archetypeTree.luau +++ /dev/null @@ -1,116 +0,0 @@ -type ComponentId = number -type EntityId = number - -export type Archetype = { - ownedEntities: { EntityId }, - - --- The component IDs that are part of this archetype, in no particular order - componentIds: { ComponentId }, - - --- Maps a component ID to its index in the storage - componentIdToStorageIndex: { [ComponentId]: number }, - - --- Maps a storage index to its component ID, useful for iterating the world - storageIndexToComponentId: { [number]: ComponentId }, - - fields: { { any } }, -} - -type Node = { - children: { [ComponentId]: Node? }, - archetype: Archetype?, -} - -type Terms = { ComponentId } - -local function newArchetypeTree() - local function createNode(): Node - local node: Node = { - children = {}, - } - - return node - end - - local root = createNode() - local function findNode(_, terms: Terms): Node - table.sort(terms) - - local node = root - for _, term in terms do - local child = node.children[term] - if child == nil then - child = createNode() - node.children[term] = child - end - - node = child - end - - return node - end - - local function findArchetypes(_, terms: Terms) - table.sort(terms) - - local archetypes = {} - local function check(node: Node, terms: Terms) - if #terms == 0 then - if node.archetype then - table.insert(archetypes, node.archetype) - end - end - - for componentId, child in node.children do - local head = terms[1] - if head then - if componentId < head then - check(child, terms) - elseif componentId == head then - local newTerms = table.clone(terms) - table.remove(newTerms, 1) - - check(child, newTerms) - end - else - check(child, terms) - end - end - end - - check(root, terms) - return archetypes - end - - local function ensureArchetype(self, terms: Terms): Archetype - local node = findNode(self, terms) - if node.archetype == nil then - node.archetype = { - ownedEntities = {}, - componentIds = terms, - componentIdToStorageIndex = {}, - storageIndexToComponentId = {}, - fields = {}, - } - - for index, componentId in terms do - node.archetype.componentIdToStorageIndex[componentId] = index - node.archetype.storageIndexToComponentId[index] = componentId - node.archetype.fields[index] = {} - end - end - - return node.archetype - end - - return table.freeze({ - findNode = findNode, - findArchetypes = findArchetypes, - ensureArchetype = ensureArchetype, - root = root, - }) -end - -local archetypeTree = newArchetypeTree() -local archetype = archetypeTree:ensureArchetype({ 1, 2, 3 }) -print(archetype) diff --git a/cspell.config.yaml b/cspell.config.yaml index 5e296a0..fe55b4e 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -6,4 +6,6 @@ language: en words: - despawn - fireboltofdeath + - jecs + - metatable - topo diff --git a/eslint.config.ts b/eslint.config.ts index 50210f0..be18815 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,19 +1,29 @@ -import style from "@isentinel/eslint-config"; +import style, { GLOB_MARKDOWN } from "@isentinel/eslint-config"; -export default style({ - rules: { - "@typescript-eslint/no-empty-interface": ["error", { allowInterfaces: true }], - "import/no-namespace": "off", - "max-lines": "error", - "max-lines-per-function": "off", - "no-param-reassign": "off", - "sonar/cognitive-complexity": "off", - "ts/no-non-null-assertion": "off", - }, - typescript: { - parserOptions: { - project: "tsconfig.eslint.json", +export default style( + { + rules: { + "import/no-namespace": "off", + "max-lines": "error", + "max-lines-per-function": "off", + "no-param-reassign": "off", + "sonar/cognitive-complexity": "off", + "ts/no-empty-object-type": [ + "error", + { + allowInterfaces: "always", + }, + ], + "ts/no-non-null-assertion": "off", + }, + typescript: { + parserOptions: { + project: "tsconfig.eslint.json", + }, + tsconfigPath: "tsconfig.eslint.json", }, - tsconfigPath: "tsconfig.eslint.json", }, -}); + { + ignores: ["src/jecs/**", GLOB_MARKDOWN], + }, +); diff --git a/package-lock.json b/package-lock.json index b0ebc02..64ea272 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,17 +11,17 @@ "dependencies": { "@flamework/core": "^1.2.3", "@rbxts/flamework-binary-serializer": "^0.6.0", - "@rbxts/jecs": "^0.3.2", "rbxts-transformer-flamework": "^1.2.3" }, "devDependencies": { "@isentinel/eslint-config": "^0.8.0", "@rbxts/compiler-types": "^3.0.0-types.0", - "@rbxts/types": "^1.0.809", - "eslint": "^9.11.0", - "jiti": "^2.1.0", + "@rbxts/types": "^1.0.813", + "bumpp": "9.7.1", + "eslint": "^9.12.0", + "jiti": "^2.3.0", "roblox-ts": "^3.0.0", - "typescript": "^5.6.2" + "typescript": "=5.5.3" } }, "node_modules/@altano/repository-tools": { @@ -2213,7 +2213,6 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", - "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -2955,9 +2954,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.11.1.tgz", - "integrity": "sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", "dev": true, "license": "MIT", "engines": { @@ -3004,6 +3003,30 @@ "resolved": "https://registry.npmjs.org/@rbxts/t/-/t-3.1.1.tgz", "integrity": "sha512-r+IvHHGLt9ZM8+cJAs5Q7ZyGaSlsbkXTaeat85FxRmjjozykHLbQ24rY/5utJbp63a5uCy3Wyl1fGv/Gk/GnIw==" }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -3018,9 +3041,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3363,6 +3386,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsdevtools/ez-spawn": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz", + "integrity": "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-me-maybe": "^1.0.1", + "cross-spawn": "^7.0.3", + "string-argv": "^0.3.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -3435,11 +3474,6 @@ "rbxts-transformer-flamework": "*" } }, - "node_modules/@rbxts/jecs": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@rbxts/jecs/-/jecs-0.3.2.tgz", - "integrity": "sha512-qgwk4QdNLMGxPS2Bei4HVqX6STtJGoVjO4fBPBi9Js386iTgeXI8SruYCPNf5v62LEOuZdnJczYART5W+UWTMA==" - }, "node_modules/@rbxts/maid": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rbxts/maid/-/maid-1.1.0.tgz", @@ -3461,9 +3495,9 @@ "integrity": "sha512-WX+ONE+ld4pG9PvRkR8OgDld9NpaV1RfXyUIw+Q2oXP/5rehkYzvt20NWtrLAP3NhMc5inYInLd+hnufey6nFw==" }, "node_modules/@rbxts/types": { - "version": "1.0.809", - "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.809.tgz", - "integrity": "sha512-COmNdE5VKq44tU1Sqbj0b70qDcUpBY1Uu5BhNb25v1yJATwcoVjsmYTyaxmRO4EVdqOATjOUBU2jbxZa4K83uw==", + "version": "1.0.813", + "resolved": "https://registry.npmjs.org/@rbxts/types/-/types-1.0.813.tgz", + "integrity": "sha512-Qpsmqnl+TNdl5AYqKFkYHyUrv652r30qK6A3GObOyDz+cvn8i+HE61mO021oq0nu1FcA4pMHbYqLSbv2CMF9Ig==", "dev": true, "license": "MIT" }, @@ -4479,6 +4513,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bumpp": { + "version": "9.7.1", + "resolved": "https://registry.npmjs.org/bumpp/-/bumpp-9.7.1.tgz", + "integrity": "sha512-Z6fhD5B8POcSkP+LIHeFQ0+vF0p/C3U+aYp3Yui748VCmsHrhJ/ZshP2970FqE93ymHrJVXTTF8/HDKrRNEYvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ez-spawn": "^3.0.4", + "c12": "^1.11.2", + "cac": "^6.7.14", + "escalade": "^3.2.0", + "fast-glob": "^3.3.2", + "js-yaml": "^4.1.0", + "jsonc-parser": "^3.3.1", + "prompts": "^2.4.2", + "semver": "^7.6.3" + }, + "bin": { + "bumpp": "bin/bumpp.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -4489,6 +4547,55 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.2.tgz", + "integrity": "sha512-oBs8a4uvSDO9dm8b7OCFW7+dgtVrwmwnrVXYzLm43ta7ep2jCn/0MhoUFygIWtxhyy6+/MG7/agvpY0U1Iemew==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.4" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/c12/node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -4509,6 +4616,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4668,6 +4782,16 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", @@ -4684,6 +4808,16 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -4804,6 +4938,16 @@ "dev": true, "license": "MIT" }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/constant-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", @@ -5163,6 +5307,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -5173,6 +5324,13 @@ "node": ">=6" } }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -5243,6 +5401,19 @@ "tslib": "^2.0.3" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.31", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.31.tgz", @@ -5495,9 +5666,9 @@ } }, "node_modules/eslint": { - "version": "9.11.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.11.1.tgz", - "integrity": "sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==", + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", "dev": true, "license": "MIT", "dependencies": { @@ -5506,11 +5677,11 @@ "@eslint/config-array": "^0.18.0", "@eslint/core": "^0.6.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.11.1", + "@eslint/js": "9.12.0", "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.3.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -5518,9 +5689,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -5530,13 +5701,11 @@ "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { @@ -7284,6 +7453,30 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7458,6 +7651,32 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7556,6 +7775,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -7587,6 +7819,26 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/git-hooks-list": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-1.0.3.tgz", @@ -7866,6 +8118,16 @@ "node": ">=10" } }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -8321,15 +8583,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -8386,6 +8639,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -8624,9 +8890,9 @@ } }, "node_modules/jiti": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.1.0.tgz", - "integrity": "sha512-Nftp80J8poC3u+93ZxpjstsgfQ5d0o5qyD6yStv32sgnWr74xRxBppEwsUoA/GIdrJpgGRkC1930YkLcAsFdSw==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz", + "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", "dev": true, "license": "MIT", "bin": { @@ -8753,6 +9019,13 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -8954,6 +9227,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -9424,6 +9704,19 @@ "node": ">=8.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9464,6 +9757,46 @@ "node": ">=8" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mlly": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", @@ -9507,6 +9840,13 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -9537,6 +9877,56 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.12.tgz", + "integrity": "sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9673,6 +10063,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", + "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", + "dev": true, + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -9683,6 +10080,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -9956,6 +10369,13 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", @@ -10328,6 +10748,17 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10759,19 +11190,6 @@ "rbxtsc": "out/CLI/cli.js" } }, - "node_modules/roblox-ts/node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -10951,6 +11369,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -11078,6 +11509,16 @@ "node": ">= 0.4" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -11215,6 +11656,19 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -11280,6 +11734,34 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11397,6 +11879,16 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", @@ -11485,10 +11977,11 @@ } }, "node_modules/typescript": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", - "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 517c93f..b34c741 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-rc.7", "description": "", "keywords": [], - "license": "ISC", + "license": "MIT", "author": "", "main": "out/init.lua", "types": "out/index.d.ts", @@ -13,7 +13,9 @@ ], "scripts": { "build": "rbxtsc", + "lint": "eslint --max-warnings 0 --flag unstable_ts_config .", "prepublishOnly": "npm run build", + "release": "bumpp && npm publish --access public", "watch": "rbxtsc -w" }, "dependencies": { @@ -24,11 +26,12 @@ "devDependencies": { "@isentinel/eslint-config": "^0.8.0", "@rbxts/compiler-types": "^3.0.0-types.0", - "@rbxts/types": "^1.0.809", - "eslint": "^9.11.0", - "jiti": "^2.1.0", + "@rbxts/types": "^1.0.813", + "bumpp": "9.7.1", + "eslint": "^9.12.0", + "jiti": "^2.3.0", "roblox-ts": "^3.0.0", - "typescript": "^5.6.2" + "typescript": "=5.5.3" }, "publishConfig": { "access": "public" diff --git a/src/hooks/use-event.ts b/src/hooks/use-event.ts index 5e65670..18fe07d 100644 --- a/src/hooks/use-event.ts +++ b/src/hooks/use-event.ts @@ -1,55 +1,56 @@ -import { Modding } from "@flamework/core"; +import type { Modding } from "@flamework/core"; + import { useHookState } from "../topo"; -type Disconnector = () => void; +type DisconnectFunction = () => void; -type Connection = { +interface Connection { Disconnect?(this: Connection): void; disconnect?(this: Connection): void; -}; +} -type Callback = (...args: T) => void; +type Callback> = (...args: T) => void; -type Signal = +type Signal> = + | ((callback: Callback) => Connection) | { connect?(this: Signal, callback: Callback): Connection; Connect?(this: Signal, callback: Callback): Connection; on?(this: Signal, callback: Callback): Connection; - } - | ((callback: Callback) => Connection); + }; -interface EventStorage { - connection: Connection | Disconnector | undefined; - events: T[]; +interface EventStorage> { + connection: Connection | DisconnectFunction | undefined; + events: Array; } -function cleanup(storage: EventStorage): boolean { +function cleanup>(storage: EventStorage): 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 { - 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."); - } + warn("No disconnect method found on the connection object."); } } + return true; } /** * A hook for easy event listening with context awareness. * + * @template T - The type of the event arguments. * @param event - The signal to listen to. * @param discriminator - A unique value to additionally key by. * @param key - An automatically generated key to store the event state. * @returns An iterable function that yields event arguments. * @metadata macro */ -export function useEvent( +export function useEvent>( event: Signal, discriminator?: unknown, key?: Modding.Caller<"uuid">, @@ -66,20 +67,16 @@ export function useEvent( 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 { - 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."); - } + error("No connect method found on the event object."); } } - return (() => { - return storage.events.shift(); - }) as IterableFunction; + return (() => storage.events.shift()) as IterableFunction; } diff --git a/src/hooks/use-throttle.ts b/src/hooks/use-throttle.ts index 89d645d..cbf1905 100644 --- a/src/hooks/use-throttle.ts +++ b/src/hooks/use-throttle.ts @@ -1,4 +1,5 @@ -import { Modding } from "@flamework/core"; +import type { Modding } from "@flamework/core"; + import { useHookState } from "../topo"; interface ThrottleStorage { @@ -13,8 +14,8 @@ function cleanup(storage: ThrottleStorage): boolean { /** * Utility for easy time-based throttling. * - * Accepts a duration and returns `true` if it has been that long since the - * last time this function returned `true`. Always returns `true` the first time. + * Accepts a duration and returns `true` if it has been that long since the last + * 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. @@ -32,7 +33,7 @@ export function useThrottle( const storage = useHookState(key, discriminator, cleanup); const currentTime = os.clock(); - if (storage.time === undefined || currentTime - storage.time >= seconds) { + if (currentTime - storage.time >= seconds) { storage.time = currentTime; storage.expiry = currentTime + seconds; return true; diff --git a/src/index.ts b/src/index.ts index 4b1da83..61d8dd6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,24 @@ -export * from "./query"; -export * from "./topo"; - export * from "./hooks/use-event"; export * from "./hooks/use-throttle"; - -export type { Wildcard, ChildOf, Entity, Id, Tag, Pair } from "./registry"; +export * from "./query"; +export type { ChildOf, Entity, Id, Pair, Tag, Wildcard } from "./registry"; export { - registry, + add, added, - removed, changed, component, - reserve, - set, + despawn, + get, + has, insert, - add, + pair, + parent, + registry, remove, - has, - get, + removed, + reserve, + set, spawn, target, - despawn, - parent, - pair, } from "./registry"; +export * from "./topo"; diff --git a/src/jecs/index.d.ts b/src/jecs/index.d.ts index 0dd40ed..f8d218a 100644 --- a/src/jecs/index.d.ts +++ b/src/jecs/index.d.ts @@ -24,7 +24,7 @@ type InferComponents = { type TupleForWorldGet = [Id] | [Id, Id] | [Id, Id, Id] | [Id, Id, Id, Id]; export class World { - constructor(); + constructor() ; entity(): Tag; component(): Entity; target(entity: Entity, relation: Entity, index?: number): Entity | undefined; @@ -35,7 +35,7 @@ export class World { remove(entity: Entity, component: Id): void; get( entity: Entity, - ...components: T + ...components: T ): FlattenTuple>>; has(entity: Entity, ...components: Id[]): boolean; contains(entity: Entity): boolean; diff --git a/src/query.ts b/src/query.ts index b2684e0..30bfd08 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,133 +1,143 @@ -import { registry, SolveKey, Id, FilterPairs, getId, Entity, component } from "./registry"; -import * as ecs from "./jecs"; -import { Modding } from "@flamework/core"; - -// Almost full credits to @fireboltofdeath for all of these types. -export interface Without { - _flamecs_without: T; -} -export interface With { - _flamecs_with: T; -} -type Skip> = T extends [unknown, ...infer R] ? R : []; - -interface Bounds { - query: Array; - with: Array; - without: Array; -} - -type BoundsTuple = T extends { length: number } & ReadonlyArray ? T : []; - -type PushBound = Omit & - Record< - K, - V extends ReadonlyArray ? [...BoundsTuple, ...V] : [...BoundsTuple, V] - >; - -type Calculate, B extends Bounds = Bounds> = T extends [] - ? { [k in keyof B]: BoundsTuple } - : T[0] extends Without - ? Calculate, PushBound> - : T[0] extends With - ? Calculate, PushBound> - : Calculate, PushBound>; - -type ToIds = T extends [] - ? undefined - : Modding.Many<{ - [k in keyof T]: SolveKey; - }>; - -type ExtractQueryTypes> = Reconstruct["query"]>>; - -type QueryHandle> = { - terms?: Id[]; - filterWith?: Id[]; - filterWithout?: Id[]; - __iter(): IterableFunction>; - /** - * Adds a pair with a runtime entity id to the query. - * The value of the relationship is appended to the end of the iterator tuple. - * - * @template P - The type of the predicate component. - * @param object - The object component ID to filter. - * @param predicate - The optional predicate component key to filter. - * @returns A new QueryHandle with the pair filter added. - * @metadata macro - */ - pair

(object: Entity, predicate?: Modding.Generic): QueryHandle<[...T, P]>; -} & IterableFunction>; - -function _queryPair, P>( - this: QueryHandle, - object: Entity, - predicate?: Modding.Generic, -): QueryHandle<[...T, 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]>; -} - -function _queryIter>( - this: QueryHandle, -): IterableFunction> { - if (this.terms) { - let ecsQuery = registry.query(...this.terms); - - if (this.filterWithout) { - ecsQuery = ecsQuery.without(...this.filterWithout); - } - - if (this.filterWith) { - ecsQuery = ecsQuery.with(...this.filterWith); - } - - return ecsQuery.iter() as IterableFunction>; - } - return (() => {}) as IterableFunction>; -} - -/** - * A world contains entities associated with some components. - * This function creates a query handle for retrieving entities that match the specified components and filters. - * - * @template T - The types of the components involved in the query. - * @param terms - The component IDs to be queried. - * @param filterWithout - The component IDs that entities must not have. - * @param filterWith - The component IDs that entities must have. - * @returns A QueryHandle for chaining additional filters or executing the query. - * @metadata macro - */ -export function query = []>( - terms?: ToIds["query"]>, - filterWithout?: ToIds["without"]>, - filterWith?: ToIds["with"]>, -): QueryHandle> { - const processedTerms = terms?.map(getId); - const processedFilterWithout = filterWithout?.map(getId); - const processedFilterWith = filterWith?.map(getId); - - const queryHandle = { - terms: processedTerms, - filterWith: processedFilterWith, - filterWithout: processedFilterWithout, - pair: _queryPair, - __iter: _queryIter, - } as QueryHandle>; - setmetatable(queryHandle, queryHandle as LuaMetatable>>); - return queryHandle; -} - -/** - * Creates a query for retrieving entities that match the specified runtime component IDs. - * It's unlikely you'll need this and using `query` is recommended. - * - * @template T - The types of the component IDs involved in the query. - * @param ids - The runtime component IDs to be queried. - * @returns A Query object that can be used to iterate over entities with the specified components. - */ -export function queryRuntimeIds(...ids: T): ecs.Query> { - return registry.query(...ids); -} +import type { Modding } from "@flamework/core"; + +import * as ecs from "./jecs"; +import type { Entity, FilterPairs, Id, SolveKey } from "./registry"; +import { component, getId, registry } from "./registry"; + +// Almost full credits to @fireboltofdeath for all of these types. +export interface Without { + _flamecs_without: T; +} +export interface With { + _flamecs_with: T; +} + +type Skip> = T extends [unknown, ...infer R] ? R : []; + +interface Bounds { + query: Array; + with: Array; + without: Array; +} + +type BoundsTuple = T extends { length: number } & ReadonlyArray ? T : []; + +type PushBound = Omit & + Record< + K, + V extends ReadonlyArray ? [...BoundsTuple, ...V] : [...BoundsTuple, V] + >; + +type Calculate, B extends Bounds = Bounds> = T extends [] + ? { [k in keyof B]: BoundsTuple } + : T[0] extends Without + ? Calculate, PushBound> + : T[0] extends With + ? Calculate, PushBound> + : Calculate, PushBound>; + +type ToIds = T extends [] + ? undefined + : Modding.Many<{ + [k in keyof T]: SolveKey; + }>; + +type ExtractQueryTypes> = Reconstruct["query"]>>; + +type QueryHandle> = { + __iter(): IterableFunction>; + filterWith?: Array; + filterWithout?: Array; + /** + * Adds a pair with a runtime entity id to the query. The value of the + * relationship is appended to the end of the iterator tuple. + * + * @template P - The type of the predicate component. + * @param object - The object component ID to filter. + * @param predicate - The optional predicate component key to filter. + * @returns A new QueryHandle with the pair filter added. + * @metadata macro + */ + pair

(object: Entity, predicate?: Modding.Generic): QueryHandle<[...T, P]>; + terms?: Array; +} & IterableFunction>; + +function queryPair, P>( + this: QueryHandle, + object: Entity, + predicate?: Modding.Generic, +): QueryHandle<[...T, 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]>; +} + +function queryIter>( + this: QueryHandle, +): IterableFunction> { + if (this.terms) { + let ecsQuery = registry.query(...this.terms); + + if (this.filterWithout) { + ecsQuery = ecsQuery.without(...this.filterWithout); + } + + if (this.filterWith) { + ecsQuery = ecsQuery.with(...this.filterWith); + } + + return ecsQuery.iter() as IterableFunction>; + } + + return (() => { + // Do nothing. + }) as IterableFunction>; +} + +/** + * A world contains entities associated with some components. This function + * creates a query handle for retrieving entities that match the specified + * components and filters. + * + * @template T - The types of the components involved in the query. + * @param terms - The component IDs to be queried. + * @param filterWithout - The component IDs that entities must not have. + * @param filterWith - The component IDs that entities must have. + * @returns A QueryHandle for chaining additional filters or executing the + * query. + * @metadata macro + */ +export function query = []>( + terms?: ToIds["query"]>, + filterWithout?: ToIds["without"]>, + filterWith?: ToIds["with"]>, +): QueryHandle> { + const processedTerms = terms?.map(getId); + const processedFilterWithout = filterWithout?.map(getId); + const processedFilterWith = filterWith?.map(getId); + + const queryHandle = { + __iter: queryIter, + filterWith: processedFilterWith, + filterWithout: processedFilterWithout, + pair: queryPair, + terms: processedTerms, + } as QueryHandle>; + setmetatable(queryHandle, queryHandle as LuaMetatable>>); + return queryHandle; +} + +/** + * Creates a query for retrieving entities that match the specified runtime + * component IDs. It's unlikely you'll need this and using `query` is + * recommended. + * + * @template T - The types of the component IDs involved in the query. + * @param ids - The runtime component IDs to be queried. + * @returns A Query object that can be used to iterate over entities with the + * specified components. + */ +export function queryRuntimeIds>(...ids: T): ecs.Query> { + return registry.query(...ids); +} diff --git a/src/registry.ts b/src/registry.ts index f33f6bd..e790cfb 100644 --- a/src/registry.ts +++ b/src/registry.ts @@ -1,300 +1,301 @@ -import type { Modding } from "@flamework/core"; - -import * as ecs from "./jecs"; -import type { Signal } from "./signal"; -import { createSignal } from "./signal"; - -export interface Wildcard {} -export interface ChildOf {} - -export type Entity = ecs.Entity; -export type Id = ecs.Id; -export type Tag = ecs.Tag; -export type Pair

= ecs.Pair; - -export type FilterPair = T extends Pair ? P : T; -export type FilterPairs> = { - [K in keyof T]: FilterPair; -}; - -export interface PairDetails { - obj: O extends undefined ? undefined : Modding.Generic; - pred: P extends undefined ? undefined : Modding.Generic; -} - -export type SolveKey = - T extends Pair ? Modding.Many> : Modding.Generic; - -export const registry = new ecs.World(); -export const signals: { - added: Record>; - changed: Record>; - removed: Record>; -} = { - added: {}, - changed: {}, - removed: {}, -}; - -export function added(id: Entity): Signal<[Entity]> { - return signals.added[id]! as Signal<[Entity]>; -} - -export function removed(id: Entity): Signal<[Entity]> { - return signals.removed[id]! as Signal<[Entity]>; -} - -export function changed(id: Entity): Signal<[Entity, T]> { - return signals.changed[id]! as Signal<[Entity, T]>; -} - -const components = new Map(); - -function hookListeners(id: Entity): void { - const addedSignal = createSignal<[Entity]>(); - const removedSignal = createSignal<[Entity]>(); - const changedSignal = createSignal<[Entity, T]>(); - signals.added[id] = addedSignal; - signals.removed[id] = removedSignal; - signals.changed[id] = changedSignal; - - registry.set(id, ecs.OnAdd, (entity) => { - addedSignal.fire(entity); - }); - registry.set(id, ecs.OnRemove, (entity) => { - removedSignal.fire(entity); - }); - registry.set(id, ecs.OnSet, (entity, data) => { - changedSignal.fire(entity, data as T); - }); -} - -/** - * Defines a component that can be added to an entity. Components can either tag - * an entity (e.g., "this entity is an NPC"), store data for an entity (e.g., - * "this entity is located at Vector3.new(10, 20, 30)"), or represent - * relationships between entities > (e.g., "bob Likes alice") that may also store - * additional data (e.g., "bob Eats 10 apples"). - * - * @template T - The type of the component. - * @param key - Flamework autogenerated key. - * @returns The component entity ID. - * @metadata macro - */ -export function component(key?: Modding.Generic): Entity { - assert(key); - let id = components.get(key) as Entity | undefined; - - if (id === undefined) { - id = registry.component(); - components.set(key, id); - hookListeners(id); - } - - return id; -} - -/** - * Registers an existing entity to the component registry. - * - * @template T - The type of the component. - * @param runtimeId - The runtime entity to be registered. - * @param key - Flamework autogenerated key. - * @metadata macro - */ -export function reserve(runtimeId: Entity, key?: Modding.Generic): void { - assert(key); - assert(!components.has(key), `A component with the key "${key}" already exists`); - components.set(key, runtimeId); - hookListeners(runtimeId); -} - -/** - * Retrieves the ID of a component or a pair relationship. - * - * @template T - The type of the component. - * @param key - Flamework autogenerated key or pair key. - * @returns The component or pair ID. - */ -export function getId(key?: SolveKey): Id> { - assert(key); - - if (typeIs(key, "table")) { - const pairKey = key as PairDetails; - return ecs.pair( - pairKey.pred !== undefined ? component(pairKey.pred) : ecs.Wildcard, - pairKey.obj !== undefined ? component(pairKey.obj) : ecs.Wildcard, - ); - } - - return component(key); -} - -/** - * Adds or updates a component for the specified entity. - * - * @template T - The type of the component. - * @param entity - The entity to modify. - * @param value - The data to set for the component. - * @param key - Flamework autogenerated key. - * @metadata macro - */ -export function set(entity: Entity, value: FilterPair, key?: SolveKey): void { - const id = getId(key); - registry.set(entity, id, value); -} - -/** - * Adds or updates multiple components for the specified entity. - * - * @template T - The type of the components. - * @param entity - The entity to modify. - * @param values - The values to set for the components. - * @param keys - Flamework autogenerated keys. - * @metadata macro - */ -export function insert>( - entity: Entity, - values: FilterPairs, - keys?: Modding.Many<{ [K in keyof T]: SolveKey }>, -): void { - assert(keys); - for (const key of keys) { - const id = getId(key); - registry.set(entity, id, values); - } -} - -/** - * Adds a component to an entity. - * - * @template T - The type of the component. - * @param entity - The entity to which the component will be added. - * @param key - Flamework autogenerated key. - * @info This function is idempotent, meaning if the entity already has the component, this operation will have no side effects. - * @metadata macro - */ -export function add(entity: Entity, key?: SolveKey): void { - const id = getId(key); - registry.add(entity, id); -} - -/** - * Removes a component from an entity. - * - * @template T - The type of the component. - * @param entity - The entity from which to remove the component. - * @param key - Flamework autogenerated key. - * @metadata macro - */ -export function remove(entity: Entity, key?: SolveKey): void { - const id = getId(key); - registry.remove(entity, id); -} - -/** - * Checks if an entity has a component. - * - * @template T - The type of the component. - * @param entity - The entity to check. - * @param key - Flamework autogenerated key. - * @returns Whether the entity has the specified component. - * @metadata macro - */ -export function has(entity: Entity, key?: SolveKey): boolean { - const id = getId(key); - return registry.has(entity, id); -} - -/** - * Retrieves the component data for an entity, or returns undefined if the - * component is not present. - * - * @template T - The type of the component. - * @param entity - The entity to retrieve the component data from. - * @param key - Flamework autogenerated key. - * @returns The component data for the specified entity, or undefined if not - * present. - * @metadata macro - */ -export function get(entity: Entity, key?: SolveKey): FilterPair | undefined { - const id = getId(key); - return registry.get(entity, id); -} - -/** - * Creates a new entity with the specified components. - * - * @template T - The type of the components. - * @param bundle - The components to add to the entity. - * @param keys - Flamework autogenerated keys. - * @returns The created entity. - * @metadata macro - */ -export function spawn>( - bundle?: FilterPairs, - keys?: Modding.Many<{ [K in keyof T]: SolveKey }>, -): Entity { - const entity = registry.entity(); - if (bundle && keys) { - for (let i = 0; i < keys.size(); i++) { - const id = getId(keys[i]); - registry.set(entity, id, bundle[i]); - } - } - - return entity; -} - -/** - * Retrieves the target entity of a relationship involving the specified entity - * and component. - * - * @template T - The type of the component. - * @param entity - The entity to get the target for. - * @param key - Flamework autogenerated key. - * @returns The target entity if a relationship exists, or undefined otherwise. - * @metadata macro - */ -export function target(entity: Entity, key?: Modding.Generic): Entity | undefined { - const id = component(key); - return registry.target(entity, id); -} - -/** - * Creates a pair relationship between a component and an entity. - * - * @template P - The type of the predicate component. - * @template O - The type of the object component. - * @param object - The object entity. - * @param predicate - The predicate component key. - * @returns The pair ID. - * @metadata macro - */ -export function pair

(object: Entity, predicate?: Modding.Generic): Pair { - const predicateId = component(predicate); - return ecs.pair(predicateId, object); -} - -/** - * Deletes the specified entity and all associated components. - * - * @param entity - The entity to delete. - */ -export function despawn(entity: Entity): void { - registry.delete(entity); -} - -/** - * Retrieves the parent entity (target of the ChildOf relationship) for the - * given entity. - * - * @param entity - The entity for which to get the parent. - * @returns The parent entity, or undefined if no parent exists. - */ -export function parent(entity: Entity): Entity | undefined { - return target(entity); -} - -reserve(ecs.Wildcard as Entity); -reserve(ecs.ChildOf as Entity); +import type { Modding } from "@flamework/core"; + +import * as ecs from "./jecs"; +import type { Signal } from "./signal"; +import { createSignal } from "./signal"; + +export interface Wildcard {} +export interface ChildOf {} + +export type Entity = ecs.Entity; +export type Id = ecs.Id; +export type Tag = ecs.Tag; +export type Pair

= ecs.Pair; + +export type FilterPair = T extends Pair ? P : T; +export type FilterPairs> = { + [K in keyof T]: FilterPair; +}; + +export interface PairDetails { + obj: O extends undefined ? undefined : Modding.Generic; + pred: P extends undefined ? undefined : Modding.Generic; +} + +export type SolveKey = + T extends Pair ? Modding.Many> : Modding.Generic; + +export const registry = new ecs.World(); +export const signals: { + added: Record>; + changed: Record>; + removed: Record>; +} = { + added: {}, + changed: {}, + removed: {}, +}; + +export function added(id: Entity): Signal<[Entity]> { + return signals.added[id]! as Signal<[Entity]>; +} + +export function removed(id: Entity): Signal<[Entity]> { + return signals.removed[id]! as Signal<[Entity]>; +} + +export function changed(id: Entity): Signal<[Entity, T]> { + return signals.changed[id]! as Signal<[Entity, T]>; +} + +const components = new Map(); + +function hookListeners(id: Entity): void { + const addedSignal = createSignal<[Entity]>(); + const removedSignal = createSignal<[Entity]>(); + const changedSignal = createSignal<[Entity, T]>(); + signals.added[id] = addedSignal; + signals.removed[id] = removedSignal; + signals.changed[id] = changedSignal; + + registry.set(id, ecs.OnAdd, entity => { + addedSignal.fire(entity); + }); + registry.set(id, ecs.OnRemove, entity => { + removedSignal.fire(entity); + }); + registry.set(id, ecs.OnSet, (entity, data) => { + changedSignal.fire(entity, data as T); + }); +} + +/** + * Defines a component that can be added to an entity. Components can either tag + * an entity (e.g., "this entity is an NPC"), store data for an entity (e.g., + * "this entity is located at Vector3.new(10, 20, 30)"), or represent + * relationships between entities > (e.g., "bob Likes alice") that + * may also store additional data (e.g., "bob Eats 10 apples"). + * + * @template T - The type of the component. + * @param key - Flamework autogenerated key. + * @returns The component entity ID. + * @metadata macro + */ +export function component(key?: Modding.Generic): Entity { + assert(key); + let id = components.get(key) as Entity | undefined; + + if (id === undefined) { + id = registry.component(); + components.set(key, id); + hookListeners(id); + } + + return id; +} + +/** + * Registers an existing entity to the component registry. + * + * @template T - The type of the component. + * @param runtimeId - The runtime entity to be registered. + * @param key - Flamework autogenerated key. + * @metadata macro + */ +export function reserve(runtimeId: Entity, key?: Modding.Generic): void { + assert(key); + assert(!components.has(key), `A component with the key "${key}" already exists`); + components.set(key, runtimeId); + hookListeners(runtimeId); +} + +/** + * Retrieves the ID of a component or a pair relationship. + * + * @template T - The type of the component. + * @param key - Flamework autogenerated key or pair key. + * @returns The component or pair ID. + */ +export function getId(key?: SolveKey): Id> { + assert(key); + + if (typeIs(key, "table")) { + const pairKey = key as PairDetails; + return ecs.pair( + pairKey.pred !== undefined! ? component(pairKey.pred) : ecs.Wildcard, + pairKey.obj !== undefined! ? component(pairKey.obj) : ecs.Wildcard, + ); + } + + return component(key); +} + +/** + * Adds or updates a component for the specified entity. + * + * @template T - The type of the component. + * @param entity - The entity to modify. + * @param value - The data to set for the component. + * @param key - Flamework autogenerated key. + * @metadata macro + */ +export function set(entity: Entity, value: FilterPair, key?: SolveKey): void { + const id = getId(key); + registry.set(entity, id, value); +} + +/** + * Adds or updates multiple components for the specified entity. + * + * @template T - The type of the components. + * @param entity - The entity to modify. + * @param values - The values to set for the components. + * @param keys - Flamework autogenerated keys. + * @metadata macro + */ +export function insert>( + entity: Entity, + values: FilterPairs, + keys?: Modding.Many<{ [K in keyof T]: SolveKey }>, +): void { + assert(keys); + for (const key of keys) { + const id = getId(key); + registry.set(entity, id, values); + } +} + +/** + * Adds a component to an entity. + * + * @template T - The type of the component. + * @param entity - The entity to which the component will be added. + * @param key - Flamework autogenerated key. + * @info This function is idempotent, meaning if the entity already has the + * component, this operation will have no side effects. + * @metadata macro + */ +export function add(entity: Entity, key?: SolveKey): void { + const id = getId(key); + registry.add(entity, id); +} + +/** + * Removes a component from an entity. + * + * @template T - The type of the component. + * @param entity - The entity from which to remove the component. + * @param key - Flamework autogenerated key. + * @metadata macro + */ +export function remove(entity: Entity, key?: SolveKey): void { + const id = getId(key); + registry.remove(entity, id); +} + +/** + * Checks if an entity has a component. + * + * @template T - The type of the component. + * @param entity - The entity to check. + * @param key - Flamework autogenerated key. + * @returns Whether the entity has the specified component. + * @metadata macro + */ +export function has(entity: Entity, key?: SolveKey): boolean { + const id = getId(key); + return registry.has(entity, id); +} + +/** + * Retrieves the component data for an entity, or returns undefined if the + * component is not present. + * + * @template T - The type of the component. + * @param entity - The entity to retrieve the component data from. + * @param key - Flamework autogenerated key. + * @returns The component data for the specified entity, or undefined if not + * present. + * @metadata macro + */ +export function get(entity: Entity, key?: SolveKey): FilterPair | undefined { + const id = getId(key); + return registry.get(entity, id); +} + +/** + * Creates a new entity with the specified components. + * + * @template T - The type of the components. + * @param bundle - The components to add to the entity. + * @param keys - Flamework autogenerated keys. + * @returns The created entity. + * @metadata macro + */ +export function spawn>( + bundle?: FilterPairs, + keys?: Modding.Many<{ [K in keyof T]: SolveKey }>, +): Entity { + const entity = registry.entity(); + if (bundle && keys) { + for (let index = 0; index < keys.size(); index++) { + const id = getId(keys[index]); + registry.set(entity, id, bundle[index]); + } + } + + return entity; +} + +/** + * Retrieves the target entity of a relationship involving the specified entity + * and component. + * + * @template T - The type of the component. + * @param entity - The entity to get the target for. + * @param key - Flamework autogenerated key. + * @returns The target entity if a relationship exists, or undefined otherwise. + * @metadata macro + */ +export function target(entity: Entity, key?: Modding.Generic): Entity | undefined { + const id = component(key); + return registry.target(entity, id); +} + +/** + * Creates a pair relationship between a component and an entity. + * + * @template P - The type of the predicate component. + * @template O - The type of the object component. + * @param object - The object entity. + * @param predicate - The predicate component key. + * @returns The pair ID. + * @metadata macro + */ +export function pair

(object: Entity, predicate?: Modding.Generic): Pair { + const predicateId = component(predicate); + return ecs.pair(predicateId, object); +} + +/** + * Deletes the specified entity and all associated components. + * + * @param entity - The entity to delete. + */ +export function despawn(entity: Entity): void { + registry.delete(entity); +} + +/** + * Retrieves the parent entity (target of the ChildOf relationship) for the + * given entity. + * + * @param entity - The entity for which to get the parent. + * @returns The parent entity, or undefined if no parent exists. + */ +export function parent(entity: Entity): Entity | undefined { + return target(entity); +} + +reserve(ecs.Wildcard as Entity); +reserve(ecs.ChildOf as Entity); diff --git a/src/signal.ts b/src/signal.ts index 95e23ed..d20d092 100644 --- a/src/signal.ts +++ b/src/signal.ts @@ -1,20 +1,20 @@ -export interface Signal { - connect(func: (...args: T) => void): void; - fire(...args: T): void; -} - -export function createSignal(): Signal { - const listeners: Array<(...args: T) => void> = []; - - return { - connect(func: (...args: T) => void): void { - listeners.push(func); - }, - - fire(...args: T): void { - for (const listener of listeners) { - listener(...args); - } - }, - }; -} +export interface Signal> { + connect(func: (...args: T) => void): void; + fire(...args: T): void; +} + +export function createSignal>(): Signal { + const listeners: Array<(...args: T) => void> = []; + + return { + connect(func: (...args: T) => void): void { + listeners.push(func); + }, + + fire(...args: T): void { + for (const listener of listeners) { + listener(...args); + } + }, + }; +} diff --git a/src/topo.ts b/src/topo.ts index 81a78f4..188a11f 100644 --- a/src/topo.ts +++ b/src/topo.ts @@ -6,11 +6,11 @@ interface State { } interface StackFrame { - node: Record>; accessedKeys: Set; + node: Record>; } -const stack: StackFrame[] = []; +const stack: Array = []; function cleanupAll(): void { const current = stack[stack.size() - 1]!; @@ -31,10 +31,9 @@ function cleanupAll(): void { * * @param node - The node to store the state for the current function. * @param func - The function to execute within the new stack frame. - * @returns - The function to execute within the new stack frame. */ export function start(node: Record>, func: () => void): void { - stack.push({ node, accessedKeys: new Set() }); + stack.push({ accessedKeys: new Set(), node }); func(); cleanupAll(); stack.pop(); @@ -43,12 +42,19 @@ export function start(node: Record>, func: () => void): v /** * Creates or retrieves a state object for a hook, keyed by a unique identifier. * - * @param key A unique string identifier for the hook state. - * @param discriminator An optional value to further distinguish different states within the same key. Defaults to the key itself. - * @param cleanup A function that determines whether the state should be cleaned up. It should return true if the state should be removed. + * @template T - The type of the state object. + * @param key - A unique string identifier for the hook state. + * @param discriminator - An optional value to further distinguish different + * states within the same key. Defaults to the key itself if undefined. + * @param cleanup - A function that determines whether the state should be + * cleaned up. It should return true if the state should be removed. * @returns The state object of type T. */ -export function useHookState(key: string, discriminator: unknown = key, cleanup: Cleanup): T { +export function useHookState(key: string, discriminator: unknown, cleanup: Cleanup): T { + if (discriminator === undefined) { + discriminator = key; + } + const current = stack[stack.size() - 1]!; let storage = current.node[key] as State | undefined;