diff --git a/.gitignore b/.gitignore index d69a925..8f592e3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ world/ players/ resources/ + +cmd/plugins/plugins.yaml config.toml # IDE configuration @@ -10,4 +12,5 @@ config.toml dragonfly/ -.env \ No newline at end of file +.env + diff --git a/examples/plugins/hcf-plugin/.gitignore b/examples/plugins/hcf-plugin/.gitignore new file mode 100644 index 0000000..f698d3f --- /dev/null +++ b/examples/plugins/hcf-plugin/.gitignore @@ -0,0 +1,177 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +plugin.sqlite + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/examples/plugins/hcf-plugin/README.md b/examples/plugins/hcf-plugin/README.md new file mode 100644 index 0000000..525ff26 --- /dev/null +++ b/examples/plugins/hcf-plugin/README.md @@ -0,0 +1,15 @@ +# hcf-plugin + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/examples/plugins/hcf-plugin/bun.lock b/examples/plugins/hcf-plugin/bun.lock new file mode 100644 index 0000000..97a2095 --- /dev/null +++ b/examples/plugins/hcf-plugin/bun.lock @@ -0,0 +1,162 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "hcf-plugin", + "dependencies": { + "@bufbuild/protobuf": "^2.2.2", + "@dragonfly/proto": "file:../../../packages/node", + "@grpc/grpc-js": "^1.14.1", + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.0", + "typescript": "^5.7.2", + }, + }, + }, + "trustedDependencies": [ + "protobufjs", + ], + "packages": { + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.10.1", "", {}, "sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg=="], + + "@dragonfly/proto": ["@dragonfly/proto@file:../../../packages/node", { "dependencies": { "@grpc/grpc-js": "^1.14.1", "typescript": "^5.9.3" }, "devDependencies": { "@types/node": "^22.0.0" }, "peerDependencies": { "@bufbuild/protobuf": "^2.2.2" } }], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "protobufjs/@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + + "protobufjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/examples/plugins/hcf-plugin/package.json b/examples/plugins/hcf-plugin/package.json new file mode 100644 index 0000000..153d54d --- /dev/null +++ b/examples/plugins/hcf-plugin/package.json @@ -0,0 +1,26 @@ +{ + "name": "ts-df-hcf", + "version": "0.1.0", + "type": "module", + "scripts": { + "postinstall": "cd ../../../packages/node && npm install", + "prebuild": "cd ../../../packages/node && npm install", + "build": "tsc", + "predev": "cd ../../../packages/node && npm install", + "dev": "bun run --hot src/index.ts", + "start": "node dist/index.js" + }, + "dependencies": { + "@grpc/grpc-js": "^1.14.1", + "@bufbuild/protobuf": "^2.2.2", + "@dragonfly/proto": "file:../../../packages/node" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.0", + "typescript": "^5.7.2" + }, + "trustedDependencies": [ + "protobufjs" + ] +} diff --git a/examples/plugins/hcf-plugin/src/db.ts b/examples/plugins/hcf-plugin/src/db.ts new file mode 100644 index 0000000..6e8c39b --- /dev/null +++ b/examples/plugins/hcf-plugin/src/db.ts @@ -0,0 +1,42 @@ +import { Database } from "bun:sqlite"; + +const dbPath = "./plugin.sqlite"; +let db: Database | null = null; + +export function getDb(): Database { + if (!db) { + db = new Database(dbPath); + } + return db; +} + +export function initializeDatabase() { + const database = getDb(); + + // Create players table + database.run(` + CREATE TABLE IF NOT EXISTS players ( + uuid TEXT PRIMARY KEY, + money INTEGER DEFAULT 0, + combat_tag_end_time INTEGER DEFAULT 0, + last_known_position_x REAL DEFAULT 0, + last_known_position_y REAL DEFAULT 0, + last_known_position_z REAL DEFAULT 0, + last_known_spawn_status INTEGER DEFAULT 1 + ); + `); + + // Create factions table + database.run(` + CREATE TABLE IF NOT EXISTS factions ( + id TEXT PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + leader_uuid TEXT NOT NULL, + members_json TEXT DEFAULT '[]', + power INTEGER DEFAULT 0, + claimed_chunks_json TEXT DEFAULT '[]' + ); + `); + + console.log("Database initialized successfully."); +} diff --git a/examples/plugins/hcf-plugin/src/index.ts b/examples/plugins/hcf-plugin/src/index.ts new file mode 100644 index 0000000..f5a1725 --- /dev/null +++ b/examples/plugins/hcf-plugin/src/index.ts @@ -0,0 +1,514 @@ +import { + PluginBase, + On, + EventType, + EventContext, + Player, + RegisterCommand, + GameMode, + PlayerJoinEvent, + PlayerMoveEvent, + PlayerQuitEvent, + PlayerAttackEntityEvent, + BlockBreakEvent, + PlayerBlockPlaceEvent, + ParamType, + Vec3, +} from '@dragonfly/proto'; +import { getDb, initializeDatabase } from './db.js'; +import { PlayerData } from './models/Player.js'; +import { FactionData } from './models/Faction.js'; +import { + WorldService, + SPAWN_CENTER_X, + SPAWN_CENTER_Y, + SPAWN_CENTER_Z, + SPAWN_RADIUS, + SPAWN_MAX_Y, + SPAWN_MIN_Y +} from './services/WorldService.js'; + + +class HCFPlugin extends PluginBase { + private worldService: WorldService; + + constructor() { + super(); + this.worldService = new WorldService(this); + } + + onLoad(): void { + console.log('[HCFPlugin] Loading...'); + initializeDatabase(); + } + + onEnable(): void { + console.log('[HCFPlugin] Enabled.'); + } + + onDisable(): void { + console.log('[HCFPlugin] Disabled.'); + // Clear all active combat walls on disable + this.worldService.clearAllCombatWalls(); + } + + // Helper method to set world time + private async setWorldTime(value: number | "day" | "night"): Promise { + let timeValue: number; + if (typeof value === "string") { + switch (value.toLowerCase()) { + case "day": timeValue = 1000; break; // Minecraft day time + case "night": timeValue = 13000; break; // Minecraft night time + default: timeValue = 0; // Default to sunrise if unknown string + } + } else { + timeValue = value; + } + + await this.sendAction('worldSetTime', { + time: timeValue, + world: { name: '', dimension: 'overworld', id: '' }, + }); + } + + @On(EventType.PLAYER_JOIN) + async onPlayerJoin(event: PlayerJoinEvent, context: EventContext) { + const player = new Player(this, event.playerUuid); + const playerData = await PlayerData.getOrCreate(event.playerUuid); + + // Always set initial spawn point + await player.teleport(SPAWN_CENTER_X, SPAWN_CENTER_Y, SPAWN_CENTER_Z); + await player.sendMessage(`§aWelcome to the HCF server, ${event.name}!`); + await player.sendMessage(`§aYour balance: $${playerData.money}.`); + + // Update player's spawn status on join + playerData.lastKnownSpawnStatus = this.worldService.isPositionInSpawn(SPAWN_CENTER_X, SPAWN_CENTER_Y, SPAWN_CENTER_Z); + // Also update last known position to spawn + playerData.lastKnownPositionX = SPAWN_CENTER_X; + playerData.lastKnownPositionY = SPAWN_CENTER_Y; + playerData.lastKnownPositionZ = SPAWN_CENTER_Z; + + await playerData.save(); + + await context.ack(); + } + + @On(EventType.PLAYER_QUIT) + async onPlayerQuit(event: PlayerQuitEvent, context: EventContext) { + console.log(`[HCFPlugin] Player ${event.name} (${event.playerUuid}) quit.`); + // Ensure to remove any active wall for this player on quit + await this.worldService.removeSpawnWall(event.playerUuid); + await context.ack(); + } + + @On(EventType.PLAYER_ATTACK_ENTITY) + async onPlayerAttackEntity(event: PlayerAttackEntityEvent, context: EventContext) { + const attackerPlayer = await PlayerData.getOrCreate(event.playerUuid); + const victimUuid = event.entity?.uuid; + + if (victimUuid) { + const victimPlayer = await PlayerData.get(victimUuid); + if (victimPlayer) { + await attackerPlayer.setCombatTag(15); + await victimPlayer.setCombatTag(15); + new Player(this, attackerPlayer.uuid).sendPopup('§cCombat Tagged!'); + new Player(this, victimPlayer.uuid).sendPopup('§cCombat Tagged!'); + } + } + await context.ack(); + } + + @On(EventType.PLAYER_MOVE) + async onPlayerMove(event: PlayerMoveEvent, context: EventContext) { + const player = new Player(this, event.playerUuid); + const playerData = await PlayerData.getOrCreate(event.playerUuid); + + const newPosX = event.position?.x ?? playerData.lastKnownPositionX; + const newPosY = event.position?.y ?? playerData.lastKnownPositionY; + const newPosZ = event.position?.z ?? playerData.lastKnownPositionZ; + + const currentPos = { x: newPosX, y: newPosY, z: newPosZ }; + const isCurrentlyInSpawn = this.worldService.isPositionInSpawn(currentPos.x, currentPos.y, currentPos.z); + const wasLastInSpawn = playerData.lastKnownSpawnStatus; + + // 1. Combat Tag Information (Console Log) + if (playerData.isCombatTagged()) { + const remaining = playerData.getCombatTagRemaining(); + } + + // 2. Spawn Entry/Exit Messages + if (isCurrentlyInSpawn && !wasLastInSpawn) { + await player.sendMessage('§aYou have entered the spawn area.'); + // Remove wall if they somehow made it in + await this.worldService.removeSpawnWall(event.playerUuid); + } else if (!isCurrentlyInSpawn && wasLastInSpawn) { + await player.sendMessage('§cYou have left the spawn area and entered the wilderness.'); + // Remove wall if they were combat tagged but now left spawn + await this.worldService.removeSpawnWall(event.playerUuid); + } + + // 3. Combat Tag Spawn Restriction and Dynamic Wall + if (playerData.isCombatTagged()) { + if (isCurrentlyInSpawn && !wasLastInSpawn) { // Combat tagged trying to enter spawn + await player.sendMessage('§cYou cannot enter spawn while combat tagged!'); + context.cancel(); // Cancel the move + await player.teleport( + playerData.lastKnownPositionX, + playerData.lastKnownPositionY, + playerData.lastKnownPositionZ + ); + await this.worldService.removeSpawnWall(event.playerUuid); // Ensure wall is removed if they fail to enter + await context.ack(); + return; // Prevent further processing + } else if (this.worldService.shouldShowSpawnWall(currentPos.x, currentPos.y, currentPos.z)) { + // Player is combat tagged, outside spawn, and close to the border, show/update wall + await this.worldService.buildSpawnWall(event.playerUuid, currentPos); + } else { // Player is combat tagged, but not near spawn border or in spawn + await this.worldService.removeSpawnWall(event.playerUuid); + } + } else { + // Player is not combat tagged, ensure no wall is shown + await this.worldService.removeSpawnWall(event.playerUuid); + } + + // Update player's last known position and spawn status + if (event.position) { + playerData.lastKnownPositionX = newPosX; + playerData.lastKnownPositionY = newPosY; + playerData.lastKnownPositionZ = newPosZ; + } + playerData.lastKnownSpawnStatus = isCurrentlyInSpawn; + await playerData.save(); + + await context.ack(); + } + + // --- Commands --- + + @RegisterCommand({ + name: 'money', + description: 'Check your money balance.', + aliases: ['bal', 'balance'], + }) + async onMoneyCommand(uuid: string, args: string[], context: EventContext) { + const player = new Player(this, uuid); + const playerData = await PlayerData.getOrCreate(uuid); + await player.sendMessage(`§aYour balance: $${playerData.money}.`); + await context.ack(); + } + + @RegisterCommand({ + name: 'time', + description: 'Set the world time.', + params: [ + { + name: 'value', + description: 'Time value (day, night, or number)', + type: ParamType.PARAM_STRING, + optional: false, + enumValues: ['day', 'night', '0', '6000', '12000', '18000'], // Common time values + }, + ], + }) + async onTimeCommand(uuid: string, args: string[], context: EventContext) { + const player = new Player(this, uuid); + if (args.length === 0) { + await player.sendMessage('§cUsage: /time set '); + await context.ack(); + return; + } + + const timeArg = args[0].toLowerCase(); + let timeValue: number | string; + + if (timeArg === 'day' || timeArg === 'night') { + timeValue = timeArg; + } else { + const parsedTime = parseInt(timeArg); + if (isNaN(parsedTime)) { + await player.sendMessage('§cInvalid time value. Use "day", "night", or a number.'); + await context.ack(); + return; + } + timeValue = parsedTime; + } + + await this.setWorldTime(timeValue as any); + await player.sendMessage(`§aWorld time set to ${timeArg}.`); + await context.ack(); + } + + @RegisterCommand({ + name: 'gamemode', + description: 'Change your gamemode.', + aliases: ['gm'], + params: [ + { + name: 'mode', + description: 'The gamemode (survival, creative, adventure, spectator)', + type: ParamType.PARAM_ENUM, + optional: false, + enumValues: ['survival', 'creative', 'adventure', 'spectator', '0', '1', '2', '3'] + }, + ] + }) + async onGamemodeCommand(uuid: string, args: string[], context: EventContext) { + const player = new Player(this, uuid); + if (args.length === 0) { + await player.sendMessage('§cUsage: /gamemode '); + await context.ack(); + return; + } + + const modeArg = args[0].toLowerCase(); + let gamemode: GameMode | undefined; + + switch (modeArg) { + case 'survival': + case '0': + gamemode = GameMode.SURVIVAL; + break; + case 'creative': + case '1': + gamemode = GameMode.CREATIVE; + break; + case 'adventure': + case '2': + gamemode = GameMode.ADVENTURE; + break; + case 'spectator': + case '3': + gamemode = GameMode.SPECTATOR; + break; + default: + await player.sendMessage('§cInvalid gamemode: ' + modeArg); + await context.ack(); + return; + } + + if (gamemode !== undefined) { + await player.setGameMode(gamemode); + await player.sendMessage(`§aYour gamemode has been set to ${modeArg}.`); + } else { + await player.sendMessage('§cAn unexpected error occurred.'); + } + await context.ack(); + } + + @RegisterCommand({ + name: 'give', + description: 'Give yourself an item.', + aliases: ['i'], + params: [ + { name: 'item', description: 'The item ID (e.g., minecraft:dirt)', type: ParamType.PARAM_STRING, optional: false }, + { name: 'amount', description: 'The amount to give (default: 1)', type: ParamType.PARAM_INT, optional: true }, + ] + }) + async onGiveCommand(uuid: string, args: string[], context: EventContext) { + const player = new Player(this, uuid); + if (args.length === 0) { + await player.sendMessage('§cUsage: /give [amount]'); + await context.ack(); + return; + } + + const itemId = args[0]; + const amount = args.length > 1 ? parseInt(args[1]) : 1; + + if (isNaN(amount) || amount <= 0) { + await player.sendMessage('§cInvalid amount. Must be a positive number.'); + await context.ack(); + return; + } + + try { + await player.giveItem(itemId, amount); + await player.sendMessage(`§aGiven ${amount} of ${itemId}.`); + } catch (error) { + console.error(`Error giving item ${itemId} to ${uuid}:`, error); + await player.sendMessage(`§cFailed to give item ${itemId}.`); + } + await context.ack(); + } + + @RegisterCommand({ + name: 'combattag', + description: 'Test command: Gives yourself combat tag for 10 seconds.', + }) + async onCombatTagCommand(uuid: string, args: string[], context: EventContext) { + const player = new Player(this, uuid); + const playerData = await PlayerData.getOrCreate(uuid); + await playerData.setCombatTag(10); + await player.sendMessage('§cYou have been combat tagged for 10 seconds!'); + await context.ack(); + } + + @RegisterCommand({ + name: 'f', + description: 'Faction commands.', + aliases: ['faction'], + params: [ + { name: 'subcommand', description: 'create, claim, info', type: ParamType.PARAM_ENUM, optional: false, enumValues: ['create', 'claim', 'info'] }, + { name: 'name', description: 'Faction name (for create/info)', type: ParamType.PARAM_STRING, optional: true }, + ] + }) + async onFactionCommand(uuid: string, args: string[], context: EventContext) { + const player = new Player(this, uuid); + const playerData = await PlayerData.getOrCreate(uuid); + + if (args.length === 0) { + await player.sendMessage('§cUsage: /f [args]'); + await context.ack(); + return; + } + + const subCommand = args[0].toLowerCase(); + + switch (subCommand) { + case 'create': + if (args.length < 2) { + await player.sendMessage('§cUsage: /f create '); + break; + } + const factionName = args[1]; + let existingFaction = await FactionData.getByName(factionName); + if (existingFaction) { + await player.sendMessage(`§cFaction '${factionName}' already exists.`); + break; + } + const newFaction = await FactionData.create(factionName, uuid); + await player.sendMessage(`§aFaction '${newFaction.name}' created!`); + break; + + case 'claim': + const playerPos = { x: playerData.lastKnownPositionX, y: playerData.lastKnownPositionY, z: playerData.lastKnownPositionZ }; + const chunkId = `${Math.floor(playerPos.x / 16)},${Math.floor(playerPos.z / 16)}`; + + let playerFaction = await FactionData.getByMember(uuid); + if (!playerFaction) { + await player.sendMessage('§cYou are not in a faction. Use /f create to make one.'); + break; + } + if (playerFaction.leaderUuid !== uuid) { + await player.sendMessage('§cOnly the faction leader can claim land.'); + break; + } + + // Check if chunk is already claimed by another faction + const allFactionsForClaimCheck = await this.getAllFactions(); + for (const otherFaction of allFactionsForClaimCheck) { + if (otherFaction.id !== playerFaction.id && otherFaction.isChunkClaimed(chunkId)) { + await player.sendMessage(`§cThis chunk is already claimed by ${otherFaction.name}.`); + break; + } + } + + if (playerFaction.addClaim(chunkId)) { + await playerFaction.save(); + await player.sendMessage(`§aClaimed chunk ${chunkId} for your faction.`); + } else { + await player.sendMessage(`§cChunk ${chunkId} is already claimed by your faction.`); + } + break; + + case 'info': + const infoFactionName = args.length > 1 ? args[1] : undefined; + let factionToDisplay: FactionData | null = null; + + if (infoFactionName) { + factionToDisplay = await FactionData.getByName(infoFactionName); + } else { + factionToDisplay = await FactionData.getByMember(uuid); + } + + if (factionToDisplay) { + await player.sendMessage(`§b--- Faction Info: ${factionToDisplay.name} ---`); + await player.sendMessage(`§bLeader: ${factionToDisplay.leaderUuid}`); // Replace with name if Player API supports it + await player.sendMessage(`§bMembers: ${factionToDisplay.members.length}`); + await player.sendMessage(`§bPower: ${factionToDisplay.power}`); + await player.sendMessage(`§bClaims: ${factionToDisplay.claimedChunks.length}`); + } else { + await player.sendMessage('§cFaction not found or you are not in one.'); + } + break; + + default: + await player.sendMessage('§cUnknown faction subcommand.'); + } + await context.ack(); + } + + // --- Event Handlers for HCF Logic --- + + @On(EventType.PLAYER_BLOCK_BREAK) + async onBlockBreak(event: BlockBreakEvent, context: EventContext) { + if (!event.position) { + await context.ack(); + return; + } + const chunkId = `${Math.floor(event.position.x! / 16)},${Math.floor(event.position.z! / 16)}`; + const allFactions = await this.getAllFactions(); + + let isClaimed = false; + let claimingFaction: FactionData | null = null; + for (const faction of allFactions) { + if (faction.isChunkClaimed(chunkId)) { + isClaimed = true; + claimingFaction = faction; + break; + } + } + + if (isClaimed && claimingFaction) { + if (!claimingFaction.members.includes(event.playerUuid)) { + await new Player(this, event.playerUuid).sendMessage(`§cThis land is claimed by ${claimingFaction.name}!`); + context.cancel(); + } + } + await context.ack(); + } + + @On(EventType.PLAYER_BLOCK_PLACE) + async onBlockPlace(event: BlockBreakEvent, context: EventContext) { // Corrected EventContext type + if (!event.position) { + await context.ack(); + return; + } + const chunkId = `${Math.floor(event.position.x! / 16)},${Math.floor(event.position.z! / 16)}`; + const allFactions = await this.getAllFactions(); + + let isClaimed = false; + let claimingFaction: FactionData | null = null; + for (const faction of allFactions) { + if (faction.isChunkClaimed(chunkId)) { + isClaimed = true; + claimingFaction = faction; + break; + } + } + + if (isClaimed && claimingFaction) { + if (!claimingFaction.members.includes(event.playerUuid)) { + await new Player(this, event.playerUuid).sendMessage(`§cThis land is claimed by ${claimingFaction.name}!`); + context.cancel(); + } + } + await context.ack(); + } + + // Helper to get all factions - Placeholder + private async getAllFactions(): Promise { + const db = getDb(); // Corrected to use getDb() + const rows = db.query(`SELECT * FROM factions`).all() as any[]; + return rows.map(row => new FactionData( + row.id, + row.name, + row.leader_uuid, + JSON.parse(row.members_json), + row.power, + JSON.parse(row.claimed_chunks_json) + )); + } +} + +new HCFPlugin(); \ No newline at end of file diff --git a/examples/plugins/hcf-plugin/src/models/Faction.ts b/examples/plugins/hcf-plugin/src/models/Faction.ts new file mode 100644 index 0000000..5f3f0c4 --- /dev/null +++ b/examples/plugins/hcf-plugin/src/models/Faction.ts @@ -0,0 +1,138 @@ +import { getDb } from '../db'; +import { randomUUID } from 'crypto'; // Bun has crypto built-in + +export class FactionData { + id: string; + name: string; + leaderUuid: string; + members: string[]; // Array of player UUIDs + power: number; + claimedChunks: string[]; // Array of chunk identifiers (e.g., "x,z") + + constructor(id: string, name: string, leaderUuid: string, members: string[] = [], power: number = 0, claimedChunks: string[] = []) { + this.id = id; + this.name = name; + this.leaderUuid = leaderUuid; + this.members = members; + this.power = power; + this.claimedChunks = claimedChunks; + } + + static async getById(id: string): Promise { + const db = getDb(); + const row = db.query(`SELECT * FROM factions WHERE id = ?`).get(id) as any; + if (row) { + return new FactionData( + row.id, + row.name, + row.leader_uuid, + JSON.parse(row.members_json), + row.power, + JSON.parse(row.claimed_chunks_json) + ); + } + return null; + } + + static async getByName(name: string): Promise { + const db = getDb(); + const row = db.query(`SELECT * FROM factions WHERE name = ?`).get(name) as any; + if (row) { + return new FactionData( + row.id, + row.name, + row.leader_uuid, + JSON.parse(row.members_json), + row.power, + JSON.parse(row.claimed_chunks_json) + ); + } + return null; + } + + static async getByMember(playerUuid: string): Promise { + const db = getDb(); + const rows = db.query(`SELECT * FROM factions`).all() as any[]; + for (const row of rows) { + const faction = new FactionData( + row.id, + row.name, + row.leader_uuid, + JSON.parse(row.members_json), + row.power, + JSON.parse(row.claimed_chunks_json) + ); + if (faction.members.includes(playerUuid)) { + return faction; + } + } + return null; + } + + static async create(name: string, leaderUuid: string): Promise { + const db = getDb(); + const id = randomUUID(); + const faction = new FactionData(id, name, leaderUuid, [leaderUuid], 1); // Leader is first member, initial power 1 + await faction.save(); + return faction; + } + + async save(): Promise { + const db = getDb(); + db.query(` + INSERT INTO factions (id, name, leader_uuid, members_json, power, claimed_chunks_json) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + name = EXCLUDED.name, + leader_uuid = EXCLUDED.leader_uuid, + members_json = EXCLUDED.members_json, + power = EXCLUDED.power, + claimed_chunks_json = EXCLUDED.claimed_chunks_json; + `).run( + this.id, + this.name, + this.leaderUuid, + JSON.stringify(this.members), + this.power, + JSON.stringify(this.claimedChunks) + ); + } + + addMember(uuid: string): void { + if (!this.members.includes(uuid)) { + this.members.push(uuid); + this.power++; // Increase power for each member + } + } + + removeMember(uuid: string): void { + const index = this.members.indexOf(uuid); + if (index > -1) { + this.members.splice(index, 1); + this.power--; // Decrease power + } + } + + addClaim(chunkId: string): boolean { + if (this.claimedChunks.includes(chunkId)) { + return false; // Already claimed + } + this.claimedChunks.push(chunkId); + this.power++; // Increase power for each claim + return true; + } + + removeClaim(chunkId: string): boolean { + const index = this.claimedChunks.indexOf(chunkId); + if (index > -1) { + this.claimedChunks.splice(index, 1); + this.power--; // Decrease power + return true; + } + return false; + } + + isChunkClaimed(chunkId: string): boolean { + return this.claimedChunks.includes(chunkId); + } +} diff --git a/examples/plugins/hcf-plugin/src/models/Player.ts b/examples/plugins/hcf-plugin/src/models/Player.ts new file mode 100644 index 0000000..42acf9a --- /dev/null +++ b/examples/plugins/hcf-plugin/src/models/Player.ts @@ -0,0 +1,105 @@ +import { getDb } from '../db.js'; + +export class PlayerData { + uuid: string; + money: number; + combatTagEndTime: number; // Unix timestamp when combat tag ends + lastKnownPositionX: number; + lastKnownPositionY: number; + lastKnownPositionZ: number; + lastKnownSpawnStatus: boolean; // true if player was last known to be in spawn + + constructor( + uuid: string, + money: number = 0, + combatTagEndTime: number = 0, + lastKnownPositionX: number = 0, + lastKnownPositionY: number = -60, // Default spawn Y + lastKnownPositionZ: number = 0, + lastKnownSpawnStatus: boolean = true // Assume new players start in spawn + ) { + this.uuid = uuid; + this.money = money; + this.combatTagEndTime = combatTagEndTime; + this.lastKnownPositionX = lastKnownPositionX; + this.lastKnownPositionY = lastKnownPositionY; + this.lastKnownPositionZ = lastKnownPositionZ; + this.lastKnownSpawnStatus = lastKnownSpawnStatus; + } + + static async get(uuid: string): Promise { + const db = getDb(); + const row = db.query(`SELECT * FROM players WHERE uuid = ?`).get(uuid) as any | null; + if (row) { + return new PlayerData( + row.uuid, + row.money, + row.combat_tag_end_time, + row.last_known_position_x, + row.last_known_position_y, + row.last_known_position_z, + Boolean(row.last_known_spawn_status) + ); + } + return null; + } + + static async getOrCreate(uuid: string): Promise { + let player = await PlayerData.get(uuid); + if (!player) { + player = new PlayerData(uuid); + await player.save(); + } + return player; + } + + async save(): Promise { + const db = getDb(); + db.query(` + INSERT INTO players (uuid, money, combat_tag_end_time, last_known_position_x, last_known_position_y, last_known_position_z, last_known_spawn_status) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(uuid) DO UPDATE SET + money = EXCLUDED.money, + combat_tag_end_time = EXCLUDED.combat_tag_end_time, + last_known_position_x = EXCLUDED.last_known_position_x, + last_known_position_y = EXCLUDED.last_known_position_y, + last_known_position_z = EXCLUDED.last_known_position_z, + last_known_spawn_status = EXCLUDED.last_known_spawn_status; + `).run( + this.uuid, + this.money, + this.combatTagEndTime, + this.lastKnownPositionX, + this.lastKnownPositionY, + this.lastKnownPositionZ, + Number(this.lastKnownSpawnStatus) // Convert boolean to number for SQLite + ); + } + + async addMoney(amount: number): Promise { + if (amount <= 0) return; + this.money += amount; + await this.save(); + } + + async removeMoney(amount: number): Promise { + if (amount <= 0 || this.money < amount) return false; + this.money -= amount; + await this.save(); + return true; + } + + async setCombatTag(durationSeconds: number): Promise { + this.combatTagEndTime = Math.floor(Date.now() / 1000) + durationSeconds; + await this.save(); + } + + isCombatTagged(): boolean { + return this.combatTagEndTime > Math.floor(Date.now() / 1000); + } + + getCombatTagRemaining(): number { + const remaining = this.combatTagEndTime - Math.floor(Date.now() / 1000); + return Math.max(0, remaining); + } +} diff --git a/examples/plugins/hcf-plugin/src/services/WorldService.ts b/examples/plugins/hcf-plugin/src/services/WorldService.ts new file mode 100644 index 0000000..9192126 --- /dev/null +++ b/examples/plugins/hcf-plugin/src/services/WorldService.ts @@ -0,0 +1,231 @@ +import { PluginBase } from '@dragonfly/proto'; +import { Player } from '@dragonfly/proto'; // For Player class to send messages +import { Action } from 'node_modules/@dragonfly/proto/dist/generated/actions.js'; + +// --- HCF Plugin Constants --- +export const SPAWN_CENTER_X = 0; +export const SPAWN_CENTER_Y = -60; +export const SPAWN_CENTER_Z = 0; +export const SPAWN_RADIUS = 50; // A radius of 50 makes a 100x100 area (x: -50 to 50, z: -50 to 50) around the center. +// Max Y for spawn vertical extent. Adjust as needed for your world's build height. +export const SPAWN_MAX_Y = 256; +export const SPAWN_MIN_Y = -64; // Min Y for spawn vertical extent + +export class WorldService { + private plugin: PluginBase; + private activeCombatWalls: Map> = new Map(); + + constructor(plugin: PluginBase) { + this.plugin = plugin; + } + + // Helper to check if a position is within the spawn area + public isPositionInSpawn(x: number, y: number, z: number): boolean { + const minX = SPAWN_CENTER_X - SPAWN_RADIUS; + const maxX = SPAWN_CENTER_X + SPAWN_RADIUS; + const minZ = SPAWN_CENTER_Z - SPAWN_RADIUS; + const maxZ = SPAWN_CENTER_Z + SPAWN_RADIUS; + + return x >= minX && x <= maxX && z >= minZ && z <= maxZ && y >= SPAWN_MIN_Y && y <= SPAWN_MAX_Y; + } + + // Clears all active combat walls for all players + public async clearAllCombatWalls(): Promise { + for (const playerUuid of this.activeCombatWalls.keys()) { + await this.removeSpawnWall(playerUuid); + } + } + + // Helper to remove the dynamic spawn wall for a player + public async removeSpawnWall(playerUuid: string): Promise { + const currentActiveBlocks = this.activeCombatWalls.get(playerUuid); + if (!currentActiveBlocks) return; + + const actions: Action[] = []; + for (const blockKey of currentActiveBlocks) { + const [x, y, z] = blockKey.split(',').map(Number); + actions.push({ + worldSetBlock: { + world: { name: '', dimension: 'overworld', id: '' }, // Assuming overworld + position: { x, y, z }, + block: { name: 'minecraft:air', properties: {} }, + }, + }); + } + this.activeCombatWalls.delete(playerUuid); // Clear entry + + if (actions.length > 0) { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { actions } + }); + } + } + + // Helper to determine if the wall should be shown around the player + public shouldShowSpawnWall(x: number, y: number, z: number): boolean { + const WALL_PROXIMITY_THRESHOLD = 20; // Player must be within 20 blocks of the spawn border + + const minX = SPAWN_CENTER_X - SPAWN_RADIUS; + const maxX = SPAWN_CENTER_X + SPAWN_RADIUS; + const minZ = SPAWN_CENTER_Z - SPAWN_RADIUS; + const maxZ = SPAWN_CENTER_Z + SPAWN_RADIUS; + + // Check if player is outside spawn but within proximity to any of its borders + const outsideSpawn = !this.isPositionInSpawn(x, y, z); + const nearXBorder = (x >= minX - WALL_PROXIMITY_THRESHOLD && x <= minX) || (x <= maxX + WALL_PROXIMITY_THRESHOLD && x >= maxX); + const nearZBorder = (z >= minZ - WALL_PROXIMITY_THRESHOLD && z <= minZ) || (z <= maxZ + WALL_PROXIMITY_THRESHOLD && z >= maxZ); + const inVerticalBounds = y >= SPAWN_MIN_Y && y <= SPAWN_MAX_Y; // Still check vertical bounds + + return outsideSpawn && (nearXBorder || nearZBorder) && inVerticalBounds; + } + + // Helper to build the dynamic spawn wall for a player + public async buildSpawnWall(playerUuid: string, playerPos: { x: number, y: number, z: number }): Promise { + let currentActiveBlocks = this.activeCombatWalls.get(playerUuid); + if (!currentActiveBlocks) { + currentActiveBlocks = new Set(); + this.activeCombatWalls.set(playerUuid, currentActiveBlocks); + } + + const actions: Action[] = []; + const newBlocks = new Set(); + const WALL_SEGMENT_WIDTH = 20; // Width of the wall segment along the perimeter + const WALL_SEGMENT_DEPTH = 1; // Depth of the wall segment extending outwards from spawn + const WALL_HEIGHT_UNITS = 20; // Vertical extent of the wall + + const wallYRangeMin = Math.floor(playerPos.y) + const wallYRangeMax = Math.min(SPAWN_MAX_Y, Math.floor(playerPos.y) + Math.ceil(WALL_HEIGHT_UNITS / 2)); + + const spawnMinX = SPAWN_CENTER_X - SPAWN_RADIUS; + const spawnMaxX = SPAWN_CENTER_X + SPAWN_RADIUS; + const spawnMinZ = SPAWN_CENTER_Z - SPAWN_RADIUS; + const spawnMaxZ = SPAWN_CENTER_Z + SPAWN_RADIUS; + + const WALL_PROXIMITY_THRESHOLD = 20; // Player must be within this many blocks of a wall to trigger segment + + const segmentsToBuild: { fixedCoord: number; isXFixed: boolean; startVaryingCoord: number; endVaryingCoord: number; depthDirection: -1 | 1; }[] = []; + + // Check West wall (fixed X = spawnMinX) + if (playerPos.x >= spawnMinX - WALL_PROXIMITY_THRESHOLD && playerPos.x <= spawnMinX) { + segmentsToBuild.push({ + fixedCoord: spawnMinX, + isXFixed: true, + startVaryingCoord: Math.floor(playerPos.z - WALL_SEGMENT_WIDTH / 2), + endVaryingCoord: Math.floor(playerPos.z + WALL_SEGMENT_WIDTH / 2), + depthDirection: -1, // Extend towards negative X (outwards from minX) + }); + } + // Check East wall (fixed X = spawnMaxX) + if (playerPos.x <= spawnMaxX + WALL_PROXIMITY_THRESHOLD && playerPos.x >= spawnMaxX) { + segmentsToBuild.push({ + fixedCoord: spawnMaxX, + isXFixed: true, + startVaryingCoord: Math.floor(playerPos.z - WALL_SEGMENT_WIDTH / 2), + endVaryingCoord: Math.floor(playerPos.z + WALL_SEGMENT_WIDTH / 2), + depthDirection: 1, // Extend towards positive X (outwards from maxX) + }); + } + // Check South wall (fixed Z = spawnMinZ) + if (playerPos.z >= spawnMinZ - WALL_PROXIMITY_THRESHOLD && playerPos.z <= spawnMinZ) { + segmentsToBuild.push({ + fixedCoord: spawnMinZ, + isXFixed: false, + startVaryingCoord: Math.floor(playerPos.x - WALL_SEGMENT_WIDTH / 2), + endVaryingCoord: Math.floor(playerPos.x + WALL_SEGMENT_WIDTH / 2), + depthDirection: -1, // Extend towards negative Z (outwards from minZ) + }); + } + // Check North wall (fixed Z = spawnMaxZ) + if (playerPos.z <= spawnMaxZ + WALL_PROXIMITY_THRESHOLD && playerPos.z >= spawnMaxZ) { + segmentsToBuild.push({ + fixedCoord: spawnMaxZ, + isXFixed: false, + startVaryingCoord: Math.floor(playerPos.x - WALL_SEGMENT_WIDTH / 2), + endVaryingCoord: Math.floor(playerPos.x + WALL_SEGMENT_WIDTH / 2), + depthDirection: 1, // Extend towards positive Z (outwards from maxZ) + }); + } + + for (const segment of segmentsToBuild) { + for (let y = wallYRangeMin; y < wallYRangeMax; y++) { + if (segment.isXFixed) { // North/South oriented wall (fixed X, varying Z) + // Clamp varying coord (z) to stay within spawn Z bounds + const clampedStartZ = Math.max(segment.startVaryingCoord, spawnMinZ); + const clampedEndZ = Math.min(segment.endVaryingCoord, spawnMaxZ); + for (let z = clampedStartZ; z <= clampedEndZ; z++) { + for (let depth = 0; depth < WALL_SEGMENT_DEPTH; depth++) { + const actualX = segment.fixedCoord + (depth * segment.depthDirection); + newBlocks.add(`${actualX},${y},${z}`); + } + } + } else { // East/West oriented wall (fixed Z, varying X) + const clampedStartX = Math.max(segment.startVaryingCoord, spawnMinX); + const clampedEndX = Math.min(segment.endVaryingCoord, spawnMaxX); + for (let x = clampedStartX; x <= clampedEndX; x++) { + for (let depth = 0; depth < WALL_SEGMENT_DEPTH; depth++) { + const actualZ = segment.fixedCoord + (depth * segment.depthDirection); + newBlocks.add(`${x},${y},${actualZ}`); + } + } + } + } + } + + // Add blocks that are new to the current view + for (const blockKey of newBlocks) { + if (!currentActiveBlocks.has(blockKey)) { + const [x, y, z] = blockKey.split(',').map(Number); + actions.push({ + worldSetBlock: { + world: { name: '', dimension: 'overworld', id: '' }, // Assuming overworld + position: { x, y, z }, + block: { name: 'minecraft:red_stained_glass', properties: {} }, // Use red glass for visibility + } + }); + currentActiveBlocks.add(blockKey); + } + } + + // Remove blocks that are no longer in view + for (const blockKey of currentActiveBlocks) { + if (!newBlocks.has(blockKey)) { + const [x, y, z] = blockKey.split(',').map(Number); + actions.push({ + worldSetBlock: { + world: { name: '', dimension: 'overworld', id: '' }, + position: { x, y, z }, + block: { name: 'minecraft:air', properties: {} }, + } + }); + } + } + this.activeCombatWalls.set(playerUuid, newBlocks); // Update active blocks + + if (actions.length > 0) { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { actions } + }); + } + } + + // World Set Time action + public async setTime(value: number | "day" | "night"): Promise { + let timeValue: number; + if (typeof value === "string") { + switch (value.toLowerCase()) { + case "day": timeValue = 1000; break; // Minecraft day time + case "night": timeValue = 13000; break; // Minecraft night time + default: timeValue = 0; // Default to sunrise if unknown string + } + } else { + timeValue = value; + } + + await this.plugin.sendAction('worldSetTime', { + time: timeValue, + world: { name: '', dimension: 'overworld', id: '' }, + }); + } +} \ No newline at end of file diff --git a/examples/plugins/hcf-plugin/tsconfig.json b/examples/plugins/hcf-plugin/tsconfig.json new file mode 100644 index 0000000..c77395c --- /dev/null +++ b/examples/plugins/hcf-plugin/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "lib": ["ES2022"], + "moduleResolution": "nodenext", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "rootDir": "./src", + "outDir": "./dist", + "declaration": true, + "sourceMap": true, + "baseUrl": ".", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/packages/node/bun.lock b/packages/node/bun.lock new file mode 100644 index 0000000..ba6f5c8 --- /dev/null +++ b/packages/node/bun.lock @@ -0,0 +1,88 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "@dragonfly/proto", + "dependencies": { + "@grpc/grpc-js": "^1.14.1", + }, + "devDependencies": { + "@types/node": "^22.0.0", + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.2.2", + }, + }, + }, + "packages": { + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.10.1", "", {}, "sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.1", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + } +} diff --git a/packages/node/package-lock.json b/packages/node/package-lock.json index af468a4..a3022c0 100644 --- a/packages/node/package-lock.json +++ b/packages/node/package-lock.json @@ -7,8 +7,12 @@ "": { "name": "@dragonfly/proto", "version": "0.1.0", + "dependencies": { + "@grpc/grpc-js": "^1.14.1", + "typescript": "^5.9.3" + }, "devDependencies": { - "typescript": "^5.6.3" + "@types/node": "^22.0.0" }, "peerDependencies": { "@bufbuild/protobuf": "^2.2.2" @@ -21,12 +25,255 @@ "license": "(Apache-2.0 AND BSD-3-Clause)", "peer": true }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.1.tgz", + "integrity": "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ==", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -34,6 +281,60 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } } } } diff --git a/packages/node/package.json b/packages/node/package.json index 747244f..095d876 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -2,24 +2,25 @@ "name": "@dragonfly/proto", "version": "0.1.0", "type": "module", - "main": "src/generated/plugin.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "exports": { - "./*": { - "types": "./src/generated/*.ts", - "import": "./src/generated/*.js" - }, ".": { - "types": "./src/generated/plugin.ts", - "import": "./src/generated/plugin.js" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" } }, "scripts": { - "build": "tsc --module ES2022 --target ES2022 --moduleResolution node --skipLibCheck --outDir src/generated src/generated/*.ts" + "build": "tsc" }, "peerDependencies": { "@bufbuild/protobuf": "^2.2.2" }, + "dependencies": { + "@grpc/grpc-js": "^1.14.1", + "typescript": "^5.9.3" + }, "devDependencies": { - "typescript": "^5.6.3" + "@types/node": "^22.0.0" } -} \ No newline at end of file +} diff --git a/packages/node/src/commands/CommandManager.ts b/packages/node/src/commands/CommandManager.ts new file mode 100644 index 0000000..d996cc4 --- /dev/null +++ b/packages/node/src/commands/CommandManager.ts @@ -0,0 +1,45 @@ +import { PluginBase } from '../plugin/PluginBase.js'; +import { CommandSpec, ParamSpec } from '../generated/command.js'; +import { COMMAND_HANDLERS, CommandOptions } from './decorators.js'; +import { EventType } from '../generated/plugin.js'; +import { EventContext } from '../events/EventManager.js'; + +export class CommandManager { + private handlers: Map = new Map(); + private definitions: CommandSpec[] = []; + + constructor(private plugin: PluginBase) { + this.registerPluginCommands(); + this.plugin.eventManager.registerHandler(EventType.COMMAND, this.handleCommandEvent.bind(this)); + } + + private registerPluginCommands() { + const proto = Object.getPrototypeOf(this.plugin); + const commands: { method: string; options: CommandOptions }[] = proto[COMMAND_HANDLERS] || []; + + for (const cmd of commands) { + const name = cmd.options.name.toLowerCase(); + this.handlers.set(name, (this.plugin as any)[cmd.method].bind(this.plugin)); + + this.definitions.push({ + name: '/' + cmd.options.name, + description: cmd.options.description || '', + aliases: cmd.options.aliases || [], + params: cmd.options.params ? cmd.options.params.map(p => ParamSpec.fromPartial(p)) : [] + }); + } + } + + public getCommandDefinitions(): CommandSpec[] { + return this.definitions; + } + + private handleCommandEvent(data: any, context: EventContext) { + const cmdName = data.command.toLowerCase(); + const handler = this.handlers.get(cmdName); + + if (handler) { + handler(data.playerUuid, data.args, context); + } + } +} diff --git a/packages/node/src/commands/decorators.ts b/packages/node/src/commands/decorators.ts new file mode 100644 index 0000000..10d950f --- /dev/null +++ b/packages/node/src/commands/decorators.ts @@ -0,0 +1,31 @@ +import { ParamType } from '../generated/command.js'; + +export const COMMAND_HANDLERS = Symbol('COMMAND_HANDLERS'); + +export interface CommandParamSpec { + name: string; + description?: string; + type: ParamType; + optional: boolean; + suffix?: string; + enumValues?: string[]; +} + +export interface CommandOptions { + name: string; + description?: string; + aliases?: string[]; + params?: CommandParamSpec[]; +} + +export function RegisterCommand(options: CommandOptions) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (!target[COMMAND_HANDLERS]) { + target[COMMAND_HANDLERS] = []; + } + target[COMMAND_HANDLERS].push({ + options, + method: propertyKey + }); + }; +} diff --git a/packages/node/src/entity/Player.ts b/packages/node/src/entity/Player.ts new file mode 100644 index 0000000..76724e9 --- /dev/null +++ b/packages/node/src/entity/Player.ts @@ -0,0 +1,133 @@ +import { PluginBase } from '../plugin/PluginBase.js'; +import { GameMode, Sound } from '../generated/common.js'; + +export class Player { + constructor(private plugin: PluginBase, public readonly uuid: string) {} + + public async sendMessage(message: string): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `msg-${Date.now()}`, + sendChat: { + targetUuid: this.uuid, + message + } + }] + } + }); + } + + public async sendTitle(title: string, subtitle: string = ''): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `title-${Date.now()}`, + sendTitle: { + playerUuid: this.uuid, + title, + subtitle + } + }] + } + }); + } + + public async sendPopup(message: string): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `popup-${Date.now()}`, + sendPopup: { + playerUuid: this.uuid, + message + } + }] + } + }); + } + + public async playSound(sound: Sound, volume: number = 1.0, pitch: number = 1.0): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `sound-${Date.now()}`, + playSound: { + playerUuid: this.uuid, + sound, + volume, + pitch + } + }] + } + }); + } + + public async teleport(x: number, y: number, z: number, yaw: number = 0, pitch: number = 0): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `tp-${Date.now()}`, + teleport: { + playerUuid: this.uuid, + position: { x, y, z }, + rotation: { x: yaw, y: pitch, z: 0 } + } + }] + } + }); + } + + public async setGameMode(mode: GameMode): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `gm-${Date.now()}`, + setGameMode: { + playerUuid: this.uuid, + gameMode: mode + } + }] + } + }); + } + + public async giveItem(name: string, count: number = 1, meta: number = 0): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `give-${Date.now()}`, + giveItem: { + playerUuid: this.uuid, + item: { + name, + count, + meta + } + } + }] + } + }); + } + + public async clearInventory(): Promise { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + actions: { + actions: [{ + correlationId: `clear-${Date.now()}`, + clearInventory: { + playerUuid: this.uuid + } + }] + } + }); + } +} diff --git a/packages/node/src/events/EventManager.ts b/packages/node/src/events/EventManager.ts new file mode 100644 index 0000000..c489ad7 --- /dev/null +++ b/packages/node/src/events/EventManager.ts @@ -0,0 +1,106 @@ +import { PluginBase } from '../plugin/PluginBase.js'; +import { HostToPlugin, EventType } from '../generated/plugin.js'; +import { EVENT_HANDLERS } from './decorators.js'; + +export class EventContext { + private handled = false; + + constructor( + public readonly event: NonNullable, + public readonly data: T, + private plugin: PluginBase + ) {} + + public get client() { + return this.plugin.getStream(); + } + + public async cancel(): Promise { + if (this.handled) return; + this.handled = true; + + if (this.event.expectsResponse) { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + eventResult: { + eventId: this.event.eventId, + cancel: true + } + }); + } + } + + public async ack(): Promise { + if (this.handled) return; + this.handled = true; + + if (this.event.expectsResponse) { + await this.plugin.send({ + pluginId: this.plugin.pluginId, + eventResult: { + eventId: this.event.eventId, + cancel: false + } + }); + } + } + + public async ackIfUnhandled(): Promise { + if (!this.handled) { + await this.ack(); + } + } +} + +export class EventManager { + private handlers: Map = new Map(); + + constructor(private plugin: PluginBase) { + this.registerPluginHandlers(); + } + + private registerPluginHandlers() { + const proto = Object.getPrototypeOf(this.plugin); + const handlers = proto[EVENT_HANDLERS] || []; + for (const handler of handlers) { + this.registerHandler(handler.type, (this.plugin as any)[handler.method].bind(this.plugin)); + } + } + + public registerHandler(type: EventType, handler: Function) { + if (!this.handlers.has(type)) { + this.handlers.set(type, []); + } + this.handlers.get(type)!.push(handler); + } + + public getSubscribedEvents(): EventType[] { + return Array.from(this.handlers.keys()); + } + + public async handleEvent(event: NonNullable) { + const handlers = this.handlers.get(event.type); + if (!handlers) return; + + let data: any = event; + + for (const key in event) { + if (key === 'eventId' || key === 'type' || key === 'expectsResponse' || key === 'immediate') continue; + const val = (event as any)[key]; + if (val !== undefined) { + data = val; + break; + } + } + + const context = new EventContext(event, data, this.plugin); + + try { + for (const handler of handlers) { + await handler(data, context); + } + } finally { + await context.ackIfUnhandled(); + } + } +} diff --git a/packages/node/src/events/decorators.ts b/packages/node/src/events/decorators.ts new file mode 100644 index 0000000..6010e52 --- /dev/null +++ b/packages/node/src/events/decorators.ts @@ -0,0 +1,15 @@ +import { EventType } from '../generated/plugin.js'; + +export const EVENT_HANDLERS = Symbol('EVENT_HANDLERS'); + +export function On(type: EventType) { + return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { + if (!target[EVENT_HANDLERS]) { + target[EVENT_HANDLERS] = []; + } + target[EVENT_HANDLERS].push({ + type, + method: propertyKey + }); + }; +} diff --git a/packages/node/src/generated/plugin.ts b/packages/node/src/generated/plugin.ts index c8ae901..340dc60 100644 --- a/packages/node/src/generated/plugin.ts +++ b/packages/node/src/generated/plugin.ts @@ -400,6 +400,32 @@ export interface HostToPlugin { serverInfo?: ServerInformationResponse | undefined; event?: EventEnvelope | undefined; actionResult?: ActionResult | undefined; + events?: EventBatch | undefined; + compressedEvents?: CompressedEventBatch | undefined; + playerMovementsPacked?: PlayerMovementsPacked | undefined; +} + +export interface CompressedEventBatch { + data: Uint8Array; + originalSize: number; +} + +export interface PlayerMovementsPacked { + moves: PackedPlayerMove[]; +} + +export interface PackedPlayerMove { + /** 16-byte UUID */ + playerUuidBytes: Uint8Array; + x: number; + y: number; + z: number; + yaw: number; + pitch: number; +} + +export interface EventBatch { + events: EventEnvelope[]; } export interface ServerInformationRequest { @@ -424,6 +450,8 @@ export interface EventEnvelope { type: EventType; /** If an event can be cancelled or mutated it expects an acknowledgement. */ expectsResponse: boolean; + /** If true, the event is sent immediately, bypassing any batching. */ + immediate: boolean; playerJoin?: PlayerJoinEvent | undefined; playerQuit?: PlayerQuitEvent | undefined; playerMove?: PlayerMoveEvent | undefined; @@ -511,6 +539,9 @@ function createBaseHostToPlugin(): HostToPlugin { serverInfo: undefined, event: undefined, actionResult: undefined, + events: undefined, + compressedEvents: undefined, + playerMovementsPacked: undefined, }; } @@ -534,6 +565,15 @@ export const HostToPlugin: MessageFns = { if (message.actionResult !== undefined) { ActionResult.encode(message.actionResult, writer.uint32(170).fork()).join(); } + if (message.events !== undefined) { + EventBatch.encode(message.events, writer.uint32(178).fork()).join(); + } + if (message.compressedEvents !== undefined) { + CompressedEventBatch.encode(message.compressedEvents, writer.uint32(186).fork()).join(); + } + if (message.playerMovementsPacked !== undefined) { + PlayerMovementsPacked.encode(message.playerMovementsPacked, writer.uint32(194).fork()).join(); + } return writer; }, @@ -592,6 +632,30 @@ export const HostToPlugin: MessageFns = { message.actionResult = ActionResult.decode(reader, reader.uint32()); continue; } + case 22: { + if (tag !== 178) { + break; + } + + message.events = EventBatch.decode(reader, reader.uint32()); + continue; + } + case 23: { + if (tag !== 186) { + break; + } + + message.compressedEvents = CompressedEventBatch.decode(reader, reader.uint32()); + continue; + } + case 24: { + if (tag !== 194) { + break; + } + + message.playerMovementsPacked = PlayerMovementsPacked.decode(reader, reader.uint32()); + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -609,6 +673,13 @@ export const HostToPlugin: MessageFns = { serverInfo: isSet(object.serverInfo) ? ServerInformationResponse.fromJSON(object.serverInfo) : undefined, event: isSet(object.event) ? EventEnvelope.fromJSON(object.event) : undefined, actionResult: isSet(object.actionResult) ? ActionResult.fromJSON(object.actionResult) : undefined, + events: isSet(object.events) ? EventBatch.fromJSON(object.events) : undefined, + compressedEvents: isSet(object.compressedEvents) + ? CompressedEventBatch.fromJSON(object.compressedEvents) + : undefined, + playerMovementsPacked: isSet(object.playerMovementsPacked) + ? PlayerMovementsPacked.fromJSON(object.playerMovementsPacked) + : undefined, }; }, @@ -632,6 +703,15 @@ export const HostToPlugin: MessageFns = { if (message.actionResult !== undefined) { obj.actionResult = ActionResult.toJSON(message.actionResult); } + if (message.events !== undefined) { + obj.events = EventBatch.toJSON(message.events); + } + if (message.compressedEvents !== undefined) { + obj.compressedEvents = CompressedEventBatch.toJSON(message.compressedEvents); + } + if (message.playerMovementsPacked !== undefined) { + obj.playerMovementsPacked = PlayerMovementsPacked.toJSON(message.playerMovementsPacked); + } return obj; }, @@ -656,6 +736,352 @@ export const HostToPlugin: MessageFns = { message.actionResult = (object.actionResult !== undefined && object.actionResult !== null) ? ActionResult.fromPartial(object.actionResult) : undefined; + message.events = (object.events !== undefined && object.events !== null) + ? EventBatch.fromPartial(object.events) + : undefined; + message.compressedEvents = (object.compressedEvents !== undefined && object.compressedEvents !== null) + ? CompressedEventBatch.fromPartial(object.compressedEvents) + : undefined; + message.playerMovementsPacked = + (object.playerMovementsPacked !== undefined && object.playerMovementsPacked !== null) + ? PlayerMovementsPacked.fromPartial(object.playerMovementsPacked) + : undefined; + return message; + }, +}; + +function createBaseCompressedEventBatch(): CompressedEventBatch { + return { data: new Uint8Array(0), originalSize: 0 }; +} + +export const CompressedEventBatch: MessageFns = { + encode(message: CompressedEventBatch, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.data.length !== 0) { + writer.uint32(10).bytes(message.data); + } + if (message.originalSize !== 0) { + writer.uint32(16).int32(message.originalSize); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): CompressedEventBatch { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseCompressedEventBatch(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.data = reader.bytes(); + continue; + } + case 2: { + if (tag !== 16) { + break; + } + + message.originalSize = reader.int32(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): CompressedEventBatch { + return { + data: isSet(object.data) ? bytesFromBase64(object.data) : new Uint8Array(0), + originalSize: isSet(object.originalSize) ? globalThis.Number(object.originalSize) : 0, + }; + }, + + toJSON(message: CompressedEventBatch): unknown { + const obj: any = {}; + if (message.data.length !== 0) { + obj.data = base64FromBytes(message.data); + } + if (message.originalSize !== 0) { + obj.originalSize = Math.round(message.originalSize); + } + return obj; + }, + + create(base?: DeepPartial): CompressedEventBatch { + return CompressedEventBatch.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): CompressedEventBatch { + const message = createBaseCompressedEventBatch(); + message.data = object.data ?? new Uint8Array(0); + message.originalSize = object.originalSize ?? 0; + return message; + }, +}; + +function createBasePlayerMovementsPacked(): PlayerMovementsPacked { + return { moves: [] }; +} + +export const PlayerMovementsPacked: MessageFns = { + encode(message: PlayerMovementsPacked, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.moves) { + PackedPlayerMove.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): PlayerMovementsPacked { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePlayerMovementsPacked(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.moves.push(PackedPlayerMove.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): PlayerMovementsPacked { + return { + moves: globalThis.Array.isArray(object?.moves) ? object.moves.map((e: any) => PackedPlayerMove.fromJSON(e)) : [], + }; + }, + + toJSON(message: PlayerMovementsPacked): unknown { + const obj: any = {}; + if (message.moves?.length) { + obj.moves = message.moves.map((e) => PackedPlayerMove.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): PlayerMovementsPacked { + return PlayerMovementsPacked.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): PlayerMovementsPacked { + const message = createBasePlayerMovementsPacked(); + message.moves = object.moves?.map((e) => PackedPlayerMove.fromPartial(e)) || []; + return message; + }, +}; + +function createBasePackedPlayerMove(): PackedPlayerMove { + return { playerUuidBytes: new Uint8Array(0), x: 0, y: 0, z: 0, yaw: 0, pitch: 0 }; +} + +export const PackedPlayerMove: MessageFns = { + encode(message: PackedPlayerMove, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.playerUuidBytes.length !== 0) { + writer.uint32(10).bytes(message.playerUuidBytes); + } + if (message.x !== 0) { + writer.uint32(21).float(message.x); + } + if (message.y !== 0) { + writer.uint32(29).float(message.y); + } + if (message.z !== 0) { + writer.uint32(37).float(message.z); + } + if (message.yaw !== 0) { + writer.uint32(45).float(message.yaw); + } + if (message.pitch !== 0) { + writer.uint32(53).float(message.pitch); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): PackedPlayerMove { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBasePackedPlayerMove(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.playerUuidBytes = reader.bytes(); + continue; + } + case 2: { + if (tag !== 21) { + break; + } + + message.x = reader.float(); + continue; + } + case 3: { + if (tag !== 29) { + break; + } + + message.y = reader.float(); + continue; + } + case 4: { + if (tag !== 37) { + break; + } + + message.z = reader.float(); + continue; + } + case 5: { + if (tag !== 45) { + break; + } + + message.yaw = reader.float(); + continue; + } + case 6: { + if (tag !== 53) { + break; + } + + message.pitch = reader.float(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): PackedPlayerMove { + return { + playerUuidBytes: isSet(object.playerUuidBytes) ? bytesFromBase64(object.playerUuidBytes) : new Uint8Array(0), + x: isSet(object.x) ? globalThis.Number(object.x) : 0, + y: isSet(object.y) ? globalThis.Number(object.y) : 0, + z: isSet(object.z) ? globalThis.Number(object.z) : 0, + yaw: isSet(object.yaw) ? globalThis.Number(object.yaw) : 0, + pitch: isSet(object.pitch) ? globalThis.Number(object.pitch) : 0, + }; + }, + + toJSON(message: PackedPlayerMove): unknown { + const obj: any = {}; + if (message.playerUuidBytes.length !== 0) { + obj.playerUuidBytes = base64FromBytes(message.playerUuidBytes); + } + if (message.x !== 0) { + obj.x = message.x; + } + if (message.y !== 0) { + obj.y = message.y; + } + if (message.z !== 0) { + obj.z = message.z; + } + if (message.yaw !== 0) { + obj.yaw = message.yaw; + } + if (message.pitch !== 0) { + obj.pitch = message.pitch; + } + return obj; + }, + + create(base?: DeepPartial): PackedPlayerMove { + return PackedPlayerMove.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): PackedPlayerMove { + const message = createBasePackedPlayerMove(); + message.playerUuidBytes = object.playerUuidBytes ?? new Uint8Array(0); + message.x = object.x ?? 0; + message.y = object.y ?? 0; + message.z = object.z ?? 0; + message.yaw = object.yaw ?? 0; + message.pitch = object.pitch ?? 0; + return message; + }, +}; + +function createBaseEventBatch(): EventBatch { + return { events: [] }; +} + +export const EventBatch: MessageFns = { + encode(message: EventBatch, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + for (const v of message.events) { + EventEnvelope.encode(v!, writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): EventBatch { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseEventBatch(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.events.push(EventEnvelope.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): EventBatch { + return { + events: globalThis.Array.isArray(object?.events) ? object.events.map((e: any) => EventEnvelope.fromJSON(e)) : [], + }; + }, + + toJSON(message: EventBatch): unknown { + const obj: any = {}; + if (message.events?.length) { + obj.events = message.events.map((e) => EventEnvelope.toJSON(e)); + } + return obj; + }, + + create(base?: DeepPartial): EventBatch { + return EventBatch.fromPartial(base ?? {}); + }, + fromPartial(object: DeepPartial): EventBatch { + const message = createBaseEventBatch(); + message.events = object.events?.map((e) => EventEnvelope.fromPartial(e)) || []; return message; }, }; @@ -902,6 +1328,7 @@ function createBaseEventEnvelope(): EventEnvelope { eventId: "", type: 0, expectsResponse: false, + immediate: false, playerJoin: undefined, playerQuit: undefined, playerMove: undefined, @@ -965,6 +1392,9 @@ export const EventEnvelope: MessageFns = { if (message.expectsResponse !== false) { writer.uint32(24).bool(message.expectsResponse); } + if (message.immediate !== false) { + writer.uint32(32).bool(message.immediate); + } if (message.playerJoin !== undefined) { PlayerJoinEvent.encode(message.playerJoin, writer.uint32(82).fork()).join(); } @@ -1146,6 +1576,14 @@ export const EventEnvelope: MessageFns = { message.expectsResponse = reader.bool(); continue; } + case 4: { + if (tag !== 32) { + break; + } + + message.immediate = reader.bool(); + continue; + } case 10: { if (tag !== 82) { break; @@ -1552,6 +1990,7 @@ export const EventEnvelope: MessageFns = { eventId: isSet(object.eventId) ? globalThis.String(object.eventId) : "", type: isSet(object.type) ? eventTypeFromJSON(object.type) : 0, expectsResponse: isSet(object.expectsResponse) ? globalThis.Boolean(object.expectsResponse) : false, + immediate: isSet(object.immediate) ? globalThis.Boolean(object.immediate) : false, playerJoin: isSet(object.playerJoin) ? PlayerJoinEvent.fromJSON(object.playerJoin) : undefined, playerQuit: isSet(object.playerQuit) ? PlayerQuitEvent.fromJSON(object.playerQuit) : undefined, playerMove: isSet(object.playerMove) ? PlayerMoveEvent.fromJSON(object.playerMove) : undefined, @@ -1669,6 +2108,9 @@ export const EventEnvelope: MessageFns = { if (message.expectsResponse !== false) { obj.expectsResponse = message.expectsResponse; } + if (message.immediate !== false) { + obj.immediate = message.immediate; + } if (message.playerJoin !== undefined) { obj.playerJoin = PlayerJoinEvent.toJSON(message.playerJoin); } @@ -1827,6 +2269,7 @@ export const EventEnvelope: MessageFns = { message.eventId = object.eventId ?? ""; message.type = object.type ?? 0; message.expectsResponse = object.expectsResponse ?? false; + message.immediate = object.immediate ?? false; message.playerJoin = (object.playerJoin !== undefined && object.playerJoin !== null) ? PlayerJoinEvent.fromPartial(object.playerJoin) : undefined; @@ -2462,6 +2905,31 @@ export const PluginDefinition = { }, } as const; +function bytesFromBase64(b64: string): Uint8Array { + if ((globalThis as any).Buffer) { + return Uint8Array.from(globalThis.Buffer.from(b64, "base64")); + } else { + const bin = globalThis.atob(b64); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; ++i) { + arr[i] = bin.charCodeAt(i); + } + return arr; + } +} + +function base64FromBytes(arr: Uint8Array): string { + if ((globalThis as any).Buffer) { + return globalThis.Buffer.from(arr).toString("base64"); + } else { + const bin: string[] = []; + arr.forEach((byte) => { + bin.push(globalThis.String.fromCharCode(byte)); + }); + return globalThis.btoa(bin.join("")); + } +} + type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; export type DeepPartial = T extends Builtin ? T diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts new file mode 100644 index 0000000..a31650c --- /dev/null +++ b/packages/node/src/index.ts @@ -0,0 +1,69 @@ +export * from './plugin/PluginBase.js'; +export * from './events/EventManager.js'; +export * from './events/decorators.js'; +export * from './commands/CommandManager.js'; +export * from './commands/decorators.js'; +export * from './entity/Player.js'; + +// Main plugin types +export * from './generated/plugin.js'; + +// Common types +export { GameMode, Vec3, Sound, CustomItemDefinition, CustomBlockDefinition } from './generated/common.js'; +export { CommandSpec, CommandEvent, ParamType, ParamSpec } from './generated/command.js'; + +// Player events +export { + BlockBreakEvent, + ChatEvent, + PlayerAttackEntityEvent, + PlayerBlockPickEvent, + PlayerBlockPlaceEvent, + PlayerChangeWorldEvent, + PlayerDeathEvent, + PlayerDiagnosticsEvent, + PlayerExperienceGainEvent, + PlayerFireExtinguishEvent, + PlayerFoodLossEvent, + PlayerHealEvent, + PlayerHeldSlotChangeEvent, + PlayerHurtEvent, + PlayerItemConsumeEvent, + PlayerItemDamageEvent, + PlayerItemDropEvent, + PlayerItemPickupEvent, + PlayerItemReleaseEvent, + PlayerItemUseEvent, + PlayerItemUseOnBlockEvent, + PlayerItemUseOnEntityEvent, + PlayerJoinEvent, + PlayerJumpEvent, + PlayerLecternPageTurnEvent, + PlayerMoveEvent, + PlayerPunchAirEvent, + PlayerQuitEvent, + PlayerRespawnEvent, + PlayerSignEditEvent, + PlayerSkinChangeEvent, + PlayerStartBreakEvent, + PlayerTeleportEvent, + PlayerToggleSneakEvent, + PlayerToggleSprintEvent, + PlayerTransferEvent, +} from "./generated/player_events.js"; + +// World events +export { + WorldBlockBurnEvent, + WorldCloseEvent, + WorldCropTrampleEvent, + WorldEntityDespawnEvent, + WorldEntitySpawnEvent, + WorldExplosionEvent, + WorldFireSpreadEvent, + WorldLeavesDecayEvent, + WorldLiquidDecayEvent, + WorldLiquidFlowEvent, + WorldLiquidHardenEvent, + WorldSoundEvent, +} from "./generated/world_events.js"; diff --git a/packages/node/src/plugin/PluginBase.ts b/packages/node/src/plugin/PluginBase.ts new file mode 100644 index 0000000..c917446 --- /dev/null +++ b/packages/node/src/plugin/PluginBase.ts @@ -0,0 +1,148 @@ +import * as grpc from '@grpc/grpc-js'; +import { HostToPlugin, PluginToHost } from '../generated/plugin.js'; +import { ActionBatch, Action } from '../generated/actions.js'; // Corrected import for Action and ActionBatch +import { CommandManager } from '../commands/CommandManager.js'; +import { EventManager } from '../events/EventManager.js'; +import { randomUUID } from 'crypto'; // Import randomUUID + +export abstract class PluginBase { + private client: grpc.Client; + private stream: grpc.ClientDuplexStream | null = null; + public readonly commandManager: CommandManager; + public readonly eventManager: EventManager; + public readonly pluginId: string; + + constructor( + private address: string = process.env.DF_PLUGIN_SERVER_ADDRESS || 'unix:///tmp/dragonfly_plugin.sock', + pluginId: string = process.env.DF_PLUGIN_ID || 'typescript-plugin' + ) { + this.pluginId = pluginId; + if (this.address.startsWith('/') && !this.address.startsWith('unix:')) { + this.address = 'unix://' + this.address; + } + + this.client = new grpc.Client(this.address, grpc.credentials.createInsecure()); + this.eventManager = new EventManager(this); + this.commandManager = new CommandManager(this); + + // Auto-run the plugin on the next tick to allow subclass initialization to complete + setTimeout(() => this.run(), 0); + } + + abstract onLoad(): void; + abstract onEnable(): void; + abstract onDisable(): void; + + public run(): void { + this.onLoad(); + console.log(`[${this.pluginId}] Connecting to ${this.address}...`); + + this.stream = this.client.makeBidiStreamRequest( + '/df.plugin.Plugin/EventStream', + (msg: PluginToHost) => { + const writer = PluginToHost.encode(msg); + return Buffer.from(writer.finish()); + }, + (buf: Buffer) => { + return HostToPlugin.decode(new Uint8Array(buf)); + } + ) as grpc.ClientDuplexStream; + + this.stream.on('data', (message: HostToPlugin) => this.handleMessage(message)); + this.stream.on('end', () => { + console.log(`[${this.pluginId}] Stream ended`); + process.exit(0); + }); + this.stream.on('error', (err) => { + console.error(`[${this.pluginId}] Stream error:`, err); + process.exit(1); + }); + + // Send Hello + const commands = this.commandManager.getCommandDefinitions(); + const hello: PluginToHost = { + pluginId: this.pluginId, + hello: { + name: this.pluginId, + version: '0.1.0', + apiVersion: 'v1', + commands: commands, + customItems: [], + customBlocks: [], + } + }; + this.stream.write(hello); + + // Subscribe + const events = this.eventManager.getSubscribedEvents(); + if (events.length > 0) { + this.stream.write({ + pluginId: this.pluginId, + subscribe: { + events: events, + } + }); + } + + this.onEnable(); + } + + private handleMessage(message: HostToPlugin): void { + if (message.hello) return; + if (message.shutdown) { + this.onDisable(); + this.stream?.end(); + return; + } + if (message.event) { + this.eventManager.handleEvent(message.event); + } + if (message.events) { + for (const event of message.events.events) { + this.eventManager.handleEvent(event); + } + } + } + + public send(msg: PluginToHost): Promise { + return new Promise((resolve, reject) => { + if (this.stream) { + this.stream.write(msg, (err?: Error | null) => { + if (err) { + console.error(`[${this.pluginId}] Error writing to stream:`, err); + return reject(err); + } + resolve(); + }); + } else { + const error = new Error(`[${this.pluginId}] specific warning: Stream not ready, cannot send message`); + console.warn(error.message); + reject(error); + } + }); + } + + public async sendAction>( + actionType: T, + actionPayload: NonNullable, + correlationId?: string + ): Promise { + const action: Action = { + correlationId: correlationId ?? randomUUID(), + [actionType]: actionPayload, // Dynamically set the action payload + }; + + const msg: PluginToHost = { + pluginId: this.pluginId, + actions: { + actions: [action], + }, + }; + + return this.send(msg); + } + + public getStream(): grpc.ClientDuplexStream | null { + return this.stream; + } +} diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json new file mode 100644 index 0000000..44e4ecc --- /dev/null +++ b/packages/node/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [ + "src/**/*" + ] +}