├── .npmrc ├── apps ├── server │ ├── .npmrc │ ├── assets │ │ ├── ignore │ │ │ └── banner.png │ │ └── custom-items │ │ │ ├── game │ │ │ └── growserver_item_wing.png │ │ │ ├── interface │ │ │ └── banner-transparent.png │ │ │ └── items-config.json │ ├── pnpm-workspace.yaml │ ├── src │ │ ├── app.ts │ │ ├── world │ │ │ ├── WorldGen.ts │ │ │ ├── tiles │ │ │ │ ├── NormalTile.ts │ │ │ │ ├── SwitcheROO.ts │ │ │ │ ├── SignTile.ts │ │ │ │ ├── WeatherTile.ts │ │ │ │ ├── HeartMonitorTile.ts │ │ │ │ └── DoorTile.ts │ │ │ └── generation │ │ │ │ └── Default.ts │ │ ├── network │ │ │ ├── actions │ │ │ │ ├── Quit.ts │ │ │ │ ├── Respawn.ts │ │ │ │ ├── QuitToExit.ts │ │ │ │ ├── RespawnSpike.ts │ │ │ │ ├── DialogReturn.ts │ │ │ │ ├── JoinRequest.ts │ │ │ │ ├── Wrench.ts │ │ │ │ ├── Drop.ts │ │ │ │ ├── index.ts │ │ │ │ ├── RefreshItemData.ts │ │ │ │ ├── Trash.ts │ │ │ │ ├── EnterGame.ts │ │ │ │ ├── Info.ts │ │ │ │ └── StoreHandler.ts │ │ │ ├── tanks │ │ │ │ ├── Disconnect.ts │ │ │ │ ├── TileUpdateData.ts │ │ │ │ ├── ItemActiveObjectReq.ts │ │ │ │ ├── AppCheckResponsePack.ts │ │ │ │ ├── SetIconState.ts │ │ │ │ ├── index.ts │ │ │ │ └── State.ts │ │ │ ├── dialogs │ │ │ │ ├── GazetteEnd.ts │ │ │ │ ├── FindItemEnd.ts │ │ │ │ ├── index.ts │ │ │ │ ├── SearchItem.ts │ │ │ │ ├── SwitcheROOEdit.ts │ │ │ │ ├── ConfirmClearWorld.ts │ │ │ │ ├── FindItem.ts │ │ │ │ ├── SignEdit.ts │ │ │ │ ├── DropEnd.ts │ │ │ │ ├── DiceEdit.ts │ │ │ │ ├── RevokeLockAccess.ts │ │ │ │ ├── DisplayBlockEdit.ts │ │ │ │ ├── DoorEdit.ts │ │ │ │ └── TrashEnd.ts │ │ │ ├── Action.ts │ │ │ └── Tank.ts │ │ ├── events │ │ │ ├── Connect.ts │ │ │ ├── Disconnect.ts │ │ │ └── Raw.ts │ │ └── command │ │ │ ├── Command.ts │ │ │ └── cmds │ │ │ ├── Ping.ts │ │ │ ├── emotes │ │ │ ├── No.ts │ │ │ ├── Dab.ts │ │ │ ├── Omg.ts │ │ │ ├── Shy.ts │ │ │ ├── Smh.ts │ │ │ ├── Yes.ts │ │ │ ├── Wave.ts │ │ │ ├── Cheer.ts │ │ │ ├── Love.ts │ │ │ ├── March.ts │ │ │ ├── Sassy.ts │ │ │ ├── Sleep.ts │ │ │ ├── Dance.ts │ │ │ ├── Grumpy.ts │ │ │ ├── Shrug.ts │ │ │ ├── Troll.ts │ │ │ ├── Dance2.ts │ │ │ ├── Facepalm.ts │ │ │ ├── Furious.ts │ │ │ ├── Rolleyes.ts │ │ │ ├── Foldarms.ts │ │ │ ├── Sad.ts │ │ │ └── Wink.ts │ │ │ ├── SaveServer.ts │ │ │ ├── Search.ts │ │ │ ├── Punch.ts │ │ │ ├── ClearWorld.ts │ │ │ ├── Sb.ts │ │ │ ├── ChangeName.ts │ │ │ ├── ChangeGrowID.ts │ │ │ ├── sdb.ts │ │ │ ├── Find.ts │ │ │ ├── Ghost.ts │ │ │ └── Help.ts │ ├── drizzle.config.ts │ ├── scripts │ │ ├── setup.ts │ │ ├── item-info │ │ │ ├── build.ts │ │ │ ├── template.ts │ │ │ ├── scraper.ts │ │ │ └── parser.ts │ │ └── update-role.ts │ ├── .gitignore │ ├── tsconfig.json │ └── package.json ├── logon │ ├── README.md │ ├── scripts │ │ └── setup.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── assets │ │ └── ssl │ │ │ ├── server.crt │ │ │ └── server.key │ └── src │ │ └── index.ts └── login-page │ ├── README.md │ ├── scripts │ └── setup.ts │ ├── .gitignore │ ├── tsconfig.json │ └── package.json ├── pnpm-workspace.yaml ├── packages ├── db │ ├── shared │ │ ├── schemas │ │ │ ├── index.ts │ │ │ ├── World.ts │ │ │ └── Player.ts │ │ └── index.ts │ ├── index.ts │ ├── drizzle.config.ts │ ├── Database.ts │ ├── tsconfig.json │ ├── package.json │ ├── handlers │ │ ├── Player.ts │ │ └── World.ts │ └── scripts │ │ └── seeds.ts ├── types │ ├── network │ │ └── events.d.ts │ ├── index.d.ts │ ├── command │ │ └── commands.d.ts │ ├── structures │ │ ├── item-pages.d.ts │ │ └── peer.d.ts │ ├── package.json │ ├── tsconfig.json │ └── misc.d.ts ├── utils │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── util │ │ └── Color.ts ├── const │ ├── package.json │ └── tsconfig.json ├── config │ ├── package.json │ ├── config.toml │ ├── tsconfig.json │ └── index.ts └── logger │ ├── package.json │ ├── tsconfig.json │ └── index.ts ├── .env.schema ├── .vscode ├── settings.json └── launch.json ├── .github ├── workflows │ ├── build.yml │ └── lint.yml ├── dependabot.yml └── FUNDING.yml ├── .dockerignore ├── Dockerfile ├── .gitignore ├── turbo.json ├── eslint.config.ts ├── docker-compose.yml ├── LICENSE ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/server/.npmrc: -------------------------------------------------------------------------------- 1 | packageManagerStrict=false -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /packages/db/shared/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Player"; 2 | export * from "./World"; 3 | -------------------------------------------------------------------------------- /packages/db/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./schemas/Player"; 2 | export * from "./schemas/World"; 3 | -------------------------------------------------------------------------------- /apps/logon/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm run dev 4 | ``` 5 | 6 | ``` 7 | open http://localhost:3000 8 | ``` 9 | -------------------------------------------------------------------------------- /apps/login-page/README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | npm install 3 | npm run dev 4 | ``` 5 | 6 | ``` 7 | open http://localhost:3000 8 | ``` 9 | -------------------------------------------------------------------------------- /apps/server/assets/ignore/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StileDevs/GrowServer/HEAD/apps/server/assets/ignore/banner.png -------------------------------------------------------------------------------- /apps/server/assets/custom-items/game/growserver_item_wing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StileDevs/GrowServer/HEAD/apps/server/assets/custom-items/game/growserver_item_wing.png -------------------------------------------------------------------------------- /apps/logon/scripts/setup.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | async function setup() { 4 | // nothing todo here for now 5 | } 6 | 7 | (async () => { 8 | await setup(); 9 | })(); 10 | -------------------------------------------------------------------------------- /apps/server/assets/custom-items/interface/banner-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StileDevs/GrowServer/HEAD/apps/server/assets/custom-items/interface/banner-transparent.png -------------------------------------------------------------------------------- /packages/types/network/events.d.ts: -------------------------------------------------------------------------------- 1 | export interface ListenerEventTypes { 2 | connect: [netID: number]; 3 | raw: [netID: number, data: Buffer]; 4 | disconnect: [netID: number]; 5 | } 6 | -------------------------------------------------------------------------------- /apps/server/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | # I dont know what this "packages" does, but it fixed the `pnpm install` problem. - Badewen 2 | packages: 3 | - "**/**" 4 | 5 | onlyBuiltDependencies: 6 | - esbuild 7 | -------------------------------------------------------------------------------- /.env.schema: -------------------------------------------------------------------------------- 1 | JWT_SECRET=SuperSecretDoNotShareToAnyoneElse 2 | DISCORD_BOT_TOKEN=Tokxxxxxxxen 3 | # postgresql://[user[:password]@][host][:port][/dbname][?options] 4 | DATABASE_URL=postgresql://growserver:ilovereimu@localhost:5432/growserver 5 | -------------------------------------------------------------------------------- /packages/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./builders/DialogBuilder"; 2 | export * from "./util/Collection"; 3 | export * from "./util/Color"; 4 | export * from "./util/ExtendBuffer"; 5 | export * from "./util/RTTEX"; 6 | export * from "./util/Seeders"; 7 | export * from "./util/Utils"; 8 | -------------------------------------------------------------------------------- /packages/db/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | export * from "./Database"; 4 | export * from "./shared"; 5 | 6 | export const dbDir: string = path.join(__dirname, "data"); 7 | export const dbPath: string = path.join(dbDir, "data.db"); 8 | export const normalizedPath = dbPath.replace(/\\/g, "/").replace(/^[A-Z]:/, ""); 9 | -------------------------------------------------------------------------------- /packages/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type * from "./command/commands"; 2 | export type * from "./network/events"; 3 | export type * from "./structures/peer"; 4 | export type * from "./structures/world"; 5 | export type * from "./structures/item-pages"; 6 | export type * from "./structures/collection"; 7 | export type * from "./misc"; 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 4 | "editor.formatOnSave": true, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": "always" 7 | }, 8 | "eslint.workingDirectories": [ 9 | { 10 | "mode": "auto" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/server/src/app.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "./core/Base"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config(); 5 | 6 | const server = new Base(); 7 | server.start(); 8 | 9 | process.on("SIGINT", () => server.shutdown()); 10 | process.on("SIGQUIT", () => server.shutdown()); 11 | process.on("SIGTERM", () => server.shutdown()); 12 | -------------------------------------------------------------------------------- /apps/server/src/world/WorldGen.ts: -------------------------------------------------------------------------------- 1 | import { WorldData } from "@growserver/types"; 2 | 3 | export abstract class WorldGen { 4 | public abstract data: WorldData; 5 | public abstract width: number; 6 | public abstract height: number; 7 | 8 | constructor(public name: string) {} 9 | 10 | public abstract generate(): Promise; 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Code 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: pnpm/action-setup@v2 10 | with: 11 | version: latest 12 | - name: Install Dependencies 13 | run: pnpm i --frozen-lockfile --ignore-scripts 14 | - name: Build 15 | run: pnpm run build 16 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: ESLint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: pnpm/action-setup@v2 10 | with: 11 | version: latest 12 | - name: Install Dependencies 13 | run: pnpm i --frozen-lockfile --ignore-scripts 14 | - name: Run ESLint 15 | run: pnpm run lint 16 | -------------------------------------------------------------------------------- /apps/login-page/scripts/setup.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { 4 | downloadMkcert, 5 | downloadWebsite, 6 | setupMkcert, 7 | setupWebsite, 8 | } from "@growserver/utils"; 9 | 10 | async function setup() { 11 | await downloadMkcert(); 12 | await downloadWebsite(); 13 | 14 | await setupMkcert(); 15 | await setupWebsite(); 16 | } 17 | 18 | (async () => { 19 | await setup(); 20 | process.exit(0); 21 | })(); 22 | -------------------------------------------------------------------------------- /packages/db/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | import { config } from "dotenv"; 3 | 4 | config({ 5 | path: "../../.env", 6 | }); 7 | 8 | export default defineConfig({ 9 | dialect: "postgresql", 10 | schema: ["./shared/schemas/index.ts"], 11 | out: "./drizzle", 12 | dbCredentials: { 13 | url: process.env.DATABASE_URL!, 14 | }, 15 | strict: false, 16 | verbose: false, 17 | }); 18 | -------------------------------------------------------------------------------- /apps/server/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | import { config } from "dotenv"; 3 | 4 | config({ 5 | path: "../../.env", 6 | }); 7 | 8 | export default defineConfig({ 9 | dialect: "postgresql", 10 | schema: ["../../packages/db/shared/schemas/index.ts"], 11 | out: "./drizzle", 12 | dbCredentials: { 13 | url: process.env.DATABASE_URL!, 14 | }, 15 | strict: false, 16 | verbose: false, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/types/command/commands.d.ts: -------------------------------------------------------------------------------- 1 | export interface CommandOptions { 2 | command: string[]; 3 | description: string; 4 | /** Cooldown command per seconds. */ 5 | cooldown: number; 6 | /** Limiting command usage. */ 7 | ratelimit: number; 8 | category: string; 9 | usage: string; 10 | example: string[]; 11 | permission: string[]; 12 | } 13 | 14 | export interface CooldownOptions { 15 | limit: number; 16 | time: number; 17 | } 18 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/Quit.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | 5 | export class Quit { 6 | constructor( 7 | public base: Base, 8 | public peer: Peer, 9 | ) {} 10 | 11 | public async execute( 12 | _action: NonEmptyObject>, 13 | ): Promise { 14 | this.peer.disconnect(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/logon/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .yarn/ 3 | !.yarn/releases 4 | .vscode/* 5 | !.vscode/launch.json 6 | !.vscode/*.code-snippets 7 | .idea/workspace.xml 8 | .idea/usage.statistics.xml 9 | .idea/shelf 10 | 11 | # deps 12 | node_modules/ 13 | 14 | # env 15 | .env 16 | .env.production 17 | 18 | # logs 19 | logs/ 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | lerna-debug.log* 26 | 27 | # misc 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/Respawn.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | 5 | export class Respawn { 6 | constructor( 7 | public base: Base, 8 | public peer: Peer, 9 | ) {} 10 | 11 | public async execute( 12 | _action: NonEmptyObject>, 13 | ): Promise { 14 | this.peer.respawn(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/login-page/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .yarn/ 3 | !.yarn/releases 4 | .vscode/* 5 | !.vscode/launch.json 6 | !.vscode/*.code-snippets 7 | .idea/workspace.xml 8 | .idea/usage.statistics.xml 9 | .idea/shelf 10 | 11 | # deps 12 | node_modules/ 13 | 14 | # env 15 | .env 16 | .env.production 17 | 18 | # logs 19 | logs/ 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | lerna-debug.log* 26 | 27 | # misc 28 | .DS_Store 29 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/QuitToExit.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../../core/Base"; 2 | import { Peer } from "../../core/Peer"; 3 | import { type NonEmptyObject } from "type-fest"; 4 | 5 | export class QuitToExit { 6 | constructor( 7 | public base: Base, 8 | public peer: Peer, 9 | ) {} 10 | 11 | public async execute( 12 | _action: NonEmptyObject>, 13 | ): Promise { 14 | this.peer.leaveWorld(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/RespawnSpike.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | 5 | export class RespawnSpike { 6 | constructor( 7 | public base: Base, 8 | public peer: Peer, 9 | ) {} 10 | 11 | public async execute( 12 | _action: NonEmptyObject>, 13 | ): Promise { 14 | this.peer.respawn(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/server/src/network/tanks/Disconnect.ts: -------------------------------------------------------------------------------- 1 | import { TankPacket } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { World } from "../../core/World"; 5 | 6 | export class Disconnect { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public tank: TankPacket, 11 | public world: World, 12 | ) {} 13 | 14 | public async execute() { 15 | this.peer.disconnect(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/const/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@growserver/const", 3 | "version": "0.0.0", 4 | "description": "GrowServer's database management", 5 | "main": "./index.ts", 6 | "keywords": [], 7 | "author": "JadlionHD ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@types/bun": "^1.3.1", 11 | "@types/node": "^24.3.1", 12 | "typescript": "^5.9.3" 13 | }, 14 | "engines": { 15 | "node": ">=18.0.0", 16 | "npm": ">=7.0.0" 17 | } 18 | } -------------------------------------------------------------------------------- /apps/server/src/world/tiles/NormalTile.ts: -------------------------------------------------------------------------------- 1 | import type { Base } from "../../core/Base"; 2 | import type { World } from "../../core/World"; 3 | import type { TileData } from "@growserver/types"; 4 | import { ExtendBuffer } from "@growserver/utils"; 5 | import { Tile } from "../Tile"; 6 | 7 | export class NormalTile extends Tile { 8 | constructor( 9 | public base: Base, 10 | public world: World, 11 | public block: TileData, 12 | ) { 13 | super(base, world, block); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/GazetteEnd.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import logger from "@growserver/logger"; 5 | export class GazzetteEnd { 6 | constructor( 7 | public base: Base, 8 | public peer: Peer, 9 | public action: NonEmptyObject>, 10 | ) {} 11 | 12 | public async execute(): Promise { 13 | logger.info("GazzetteEnd fired 🔥🔥"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@growserver/config", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "GrowServer config files", 6 | "main": "./index.ts", 7 | "exports": { 8 | ".": "./index.ts" 9 | }, 10 | "author": "JadlionHD ", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@types/bun": "^1.2.21", 14 | "@types/node": "^24.9.2", 15 | "typescript": "^5.9.3" 16 | }, 17 | "engines": { 18 | "node": ">=18" 19 | }, 20 | "dependencies": { 21 | "smol-toml": "^1.4.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/types/structures/item-pages.d.ts: -------------------------------------------------------------------------------- 1 | import type { ItemDefinition } from "grow-items"; 2 | 3 | export interface ItemsInfo { 4 | id: number; 5 | name: string; 6 | desc: string; 7 | recipe?: Recipe; 8 | func?: ItemInfoFunc; 9 | playMods?: string[]; 10 | chi?: "earth" | "wind" | "fire" | "water" | ""; 11 | } 12 | 13 | export interface ItemInfoFunc { 14 | add: string; 15 | rem: string; 16 | } 17 | 18 | export interface Recipe { 19 | splice: number[]; 20 | } 21 | 22 | export interface ItemsPage { 23 | text?: string; 24 | items: ItemDefinition[]; 25 | } 26 | -------------------------------------------------------------------------------- /apps/server/scripts/setup.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import fs from "fs/promises"; 4 | import { existsSync } from "fs"; 5 | import { buildItemsInfo } from "./item-info/build"; 6 | import { Database, dbDir, dbPath } from "@growserver/db"; 7 | 8 | async function setup() { 9 | const isData = existsSync(dbDir); 10 | if (!isData) { 11 | await fs.mkdir(dbDir); 12 | await fs.writeFile(dbPath, Buffer.alloc(0)); 13 | } 14 | 15 | const db = new Database(); 16 | await db.setup(); 17 | await buildItemsInfo(); 18 | process.exit(0); 19 | } 20 | 21 | (async () => { 22 | await setup(); 23 | })(); 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | open-pull-requests-limit: 20 13 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@growserver/types", 3 | "version": "0.0.0", 4 | "description": "GrowServer types", 5 | "main": "./index.d.ts", 6 | "types": "./index.d.ts", 7 | "keywords": [], 8 | "author": "JadlionHD ", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "@types/bun": "^1.3.1", 12 | "@types/node": "^24.3.1", 13 | "grow-items": "^1.3.0", 14 | "typescript": "^5.9.3" 15 | }, 16 | "engines": { 17 | "node": ">=18.0.0", 18 | "npm": ">=7.0.0" 19 | }, 20 | "dependencies": { 21 | "@growserver/const": "workspace:*" 22 | } 23 | } -------------------------------------------------------------------------------- /packages/logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@growserver/logger", 3 | "version": "0.0.0", 4 | "description": "GrowServer logging output", 5 | "main": "./index.ts", 6 | "keywords": [], 7 | "author": "JadlionHD ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@types/bun": "^1.3.1", 11 | "@types/node": "^24.3.1", 12 | "typescript": "^5.9.3" 13 | }, 14 | "engines": { 15 | "node": ">=18.0.0", 16 | "npm": ">=7.0.0" 17 | }, 18 | "dependencies": { 19 | "@growserver/config": "workspace:*", 20 | "pino": "^10.1.0", 21 | "pino-pretty": "^13.1.2" 22 | } 23 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | dist-ssr 5 | yarn.lock 6 | data 7 | assets/website/ 8 | assets/build.zip 9 | build 10 | .cache 11 | data.db 12 | data 13 | 14 | 15 | 16 | # Ignore ssl 17 | assets/ssl/_wildcard.growserver.app.pem 18 | assets/ssl/_wildcard.growserver.app-key.pem 19 | 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | pnpm-debug.log* 27 | lerna-debug.log* 28 | 29 | *.local 30 | 31 | # Editor directories and files 32 | !.vscode/extensions.json 33 | .idea 34 | .DS_Store 35 | *.suo 36 | *.ntvs* 37 | *.njsproj 38 | *.sln 39 | *.sw? 40 | 41 | items_info_new.json 42 | .vs/ 43 | -------------------------------------------------------------------------------- /apps/server/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | dist 4 | dist-ssr 5 | yarn.lock 6 | data 7 | assets/website/ 8 | assets/build.zip 9 | build 10 | .cache 11 | data.db 12 | data 13 | 14 | 15 | 16 | # Ignore ssl 17 | assets/ssl/_wildcard.growserver.app.pem 18 | assets/ssl/_wildcard.growserver.app-key.pem 19 | 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | pnpm-debug.log* 27 | lerna-debug.log* 28 | 29 | *.local 30 | 31 | # Editor directories and files 32 | !.vscode/extensions.json 33 | .idea 34 | .DS_Store 35 | *.suo 36 | *.ntvs* 37 | *.njsproj 38 | *.sln 39 | *.sw? 40 | 41 | items_info_new.json 42 | .vs/ 43 | -------------------------------------------------------------------------------- /packages/config/config.toml: -------------------------------------------------------------------------------- 1 | [web] 2 | development = true 3 | address = "127.0.0.1" 4 | port = 443 5 | ports = [17091] 6 | loginUrl = "login.growserver.app:8080" 7 | cdnUrl = "growserver-cache.netlify.app" 8 | 9 | [web.maintenance] 10 | enable = false 11 | message = "Maintenance Woi" 12 | 13 | [web.tls] 14 | key = "./assets/ssl/server.key" 15 | cert = "./assets/ssl/server.crt" 16 | 17 | [webFrontend] 18 | root = "./.cache/website" 19 | port = 8080 20 | 21 | [webFrontend.tls] 22 | key = "./.cache/ssl/_wildcard.growserver.app-key.pem" 23 | cert = "./.cache/ssl/_wildcard.growserver.app.pem" 24 | 25 | [server] 26 | bypassVersionCheck = true 27 | logLevel = "info" -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS mkcert-build 2 | 3 | 4 | RUN apk --no-cache add curl 5 | RUN curl -JLO "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" && \ 6 | chmod +x mkcert-v1.4.4-linux-amd64 7 | 8 | FROM node:22-alpine 9 | 10 | WORKDIR /app 11 | COPY . . 12 | COPY --from=mkcert-build /mkcert-v1.4.4-linux-amd64 /app/.cache/bin/mkcert 13 | 14 | VOLUME /app 15 | 16 | RUN corepack enable && corepack prepare pnpm@latest 17 | 18 | RUN pnpm install --frozen-lockfile 19 | 20 | EXPOSE 80/tcp 21 | EXPOSE 8080/tcp 22 | EXPOSE 443/tcp 23 | EXPOSE 17091/udp 24 | 25 | # RUN chmod +x /app/.cache/bin/mkcert 26 | 27 | CMD ["pnpm", "dev"] -------------------------------------------------------------------------------- /apps/server/src/events/Connect.ts: -------------------------------------------------------------------------------- 1 | import { TextPacket, Variant } from "growtopia.js"; 2 | import { Base } from "../core/Base"; 3 | import { Peer } from "../core/Peer"; 4 | import logger from "@growserver/logger"; 5 | 6 | export class ConnectListener { 7 | constructor(public base: Base) { 8 | logger.info('Listening ENet "connect" event'); 9 | } 10 | 11 | public run(netID: number): void { 12 | const peer = new Peer(this.base, netID); 13 | const peerAddr = peer.enet; 14 | 15 | logger.info(`Peer ${netID} [/${peerAddr.ip}:${peerAddr.port}] connected`); 16 | this.base.cache.peers.set(netID, peer.data); 17 | 18 | peer.send(TextPacket.from(0x1)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/server/src/command/Command.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../core/Base"; 2 | import { Peer } from "../core/Peer"; 3 | import type { CommandOptions } from "@growserver/types"; 4 | 5 | export class Command { 6 | public opt: CommandOptions; 7 | 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public text: string, 12 | public args: string[], 13 | ) { 14 | this.opt = { 15 | command: [], 16 | description: "", 17 | cooldown: 1, 18 | ratelimit: 1, 19 | category: "", 20 | usage: "", 21 | example: [], 22 | permission: [], 23 | }; 24 | } 25 | 26 | public async execute(): Promise {} 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | # GrowServer ignores 31 | .cache 32 | data 33 | 34 | # Debug 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | 39 | # Misc 40 | .DS_Store 41 | *.pem 42 | 43 | # Custom ignores 44 | setup-port-forwarding.ps1 45 | tools/ 46 | update-hosts.ps1 47 | -------------------------------------------------------------------------------- /apps/server/src/network/tanks/TileUpdateData.ts: -------------------------------------------------------------------------------- 1 | import { TankPacket } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { World } from "../../core/World"; 5 | import { TileData } from "@growserver/types"; 6 | 7 | export class TileUpdateData { 8 | private pos: number; 9 | private block: TileData; 10 | 11 | constructor( 12 | public base: Base, 13 | public peer: Peer, 14 | public tank: TankPacket, 15 | public world: World, 16 | ) { 17 | this.pos = 18 | (this.tank.data?.xPunch as number) + 19 | (this.tank.data?.yPunch as number) * this.world.data.width; 20 | this.block = this.world.data.blocks[this.pos]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/db/Database.ts: -------------------------------------------------------------------------------- 1 | import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js"; 2 | import postgres from "postgres"; 3 | 4 | import { WorldDB } from "./handlers/World"; 5 | import { PlayerDB } from "./handlers/Player"; 6 | import { setupSeeds } from "./scripts/seeds"; 7 | 8 | export class Database { 9 | public db: PostgresJsDatabase>; 10 | public players; 11 | public worlds; 12 | 13 | constructor() { 14 | const connection = postgres(process.env.DATABASE_URL!); 15 | this.db = drizzle(connection, { logger: false }); 16 | 17 | this.players = new PlayerDB(this.db); 18 | this.worlds = new WorldDB(this.db); 19 | } 20 | 21 | public async setup() { 22 | await setupSeeds(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JadlionHD 2 | patreon: # Replace with a single Patreon username 3 | open_collective: # Replace with a single Open Collective username 4 | ko_fi: # Replace with a single Ko-fi username 5 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 6 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 7 | liberapay: # Replace with a single Liberapay username 8 | issuehunt: # Replace with a single IssueHunt username 9 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 10 | polar: # Replace with a single Polar username 11 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 12 | thanks_dev: # Replace with a single thanks.dev username 13 | custom: ["https://tako.id/jadlionhd"] 14 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.com/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "lint:fix": { 14 | "dependsOn": ["^lint:fix"] 15 | }, 16 | "check-types": { 17 | "dependsOn": ["^check-types"] 18 | }, 19 | "dev": { 20 | "cache": false, 21 | "persistent": true, 22 | "interactive": true 23 | }, 24 | "clean": { 25 | "cache": false 26 | }, 27 | "setup": { 28 | "dependsOn": ["^setup"], 29 | "cache": false, 30 | "persistent": true, 31 | "interactive": true 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import stylisticTs from "@stylistic/eslint-plugin-ts"; 4 | 5 | export default tseslint.config( 6 | eslint.configs.recommended, 7 | tseslint.configs.recommended, 8 | { 9 | ignores: ["node_modules/*", "dist/*", "build/*", "data/*", "scripts/*"], 10 | plugins: { 11 | "@stylistic/ts": stylisticTs, 12 | }, 13 | rules: { 14 | indent: "off", 15 | "no-unused-vars": "off", 16 | "keyword-spacing": "error", 17 | "key-spacing": [ 18 | "error", 19 | { 20 | align: "value", 21 | }, 22 | ], 23 | "@typescript-eslint/no-unused-vars": "off", 24 | "@stylistic/ts/indent": [ 25 | "error", 26 | 2, 27 | { 28 | SwitchCase: 1, 29 | }, 30 | ], 31 | }, 32 | }, 33 | ); 34 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@growserver/utils", 3 | "version": "0.0.0", 4 | "description": "GrowServer utilities function", 5 | "main": "./index.ts", 6 | "keywords": [], 7 | "author": "JadlionHD ", 8 | "license": "MIT", 9 | "devDependencies": { 10 | "@types/bun": "^1.3.1", 11 | "@types/decompress": "^4.2.7", 12 | "@types/node": "^24.3.1", 13 | "growtopia.js": "^2.1.6", 14 | "typescript": "^5.9.3" 15 | }, 16 | "engines": { 17 | "node": ">=18.0.0", 18 | "npm": ">=7.0.0" 19 | }, 20 | "dependencies": { 21 | "@growserver/config": "workspace:*", 22 | "@growserver/const": "workspace:*", 23 | "@growserver/logger": "workspace:*", 24 | "@growserver/types": "workspace:*", 25 | "dayjs": "^1.11.19", 26 | "decompress": "^4.2.1", 27 | "imagescript": "^1.3.1", 28 | "ky": "^1.10.0" 29 | } 30 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | server: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | restart: unless-stopped 7 | ports: 8 | - "80:80" 9 | - "8080:8080" 10 | - "443:443" 11 | - "17091:17091/udp" 12 | depends_on: 13 | - db 14 | - redis 15 | environment: 16 | - NODE_ENV=development 17 | db: 18 | image: postgres:17 19 | restart: unless-stopped 20 | environment: 21 | POSTGRES_USER: growserver 22 | POSTGRES_PASSWORD: ilovereimu 23 | POSTGRES_DB: growserver 24 | volumes: 25 | - postgres_data:/var/lib/postgresql/data 26 | ports: 27 | - "5432:5432" 28 | redis: 29 | image: redis:7-alpine 30 | restart: unless-stopped 31 | volumes: 32 | - redis_data:/var/lib/redis/data 33 | ports: 34 | - "6379:6379" 35 | 36 | volumes: 37 | postgres_data: 38 | redis_data: 39 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/DialogReturn.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { DialogMap } from "../dialogs/index"; 5 | import logger from "@growserver/logger"; 6 | 7 | export class DialogReturn { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | ) {} 12 | 13 | public async execute( 14 | action: NonEmptyObject>, 15 | ): Promise { 16 | try { 17 | const Class = DialogMap[action.dialog_name]; 18 | 19 | if (!Class) 20 | throw new Error( 21 | `No Dialog class found with dialog name ${action.dialog_name}`, 22 | ); 23 | 24 | const dialog = new Class(this.base, this.peer, action); 25 | await dialog.execute(); 26 | } catch (e) { 27 | logger.warn(e); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/server/assets/custom-items/items-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets": [ 3 | { 4 | "id": 8900, 5 | "item": { 6 | "extraFile": { 7 | "fileName": "banner-transparent.rttex", 8 | "pathAsset": "interface/banner-transparent.png", 9 | "pathResult": "interface/banner-transparent.rttex" 10 | } 11 | }, 12 | "storePath": "interface" 13 | }, 14 | { 15 | "id": 5136, 16 | "item": { 17 | "name": "Testing Wing", 18 | "textureX": 1, 19 | "textureY": 0, 20 | "type": 20, 21 | "bodyPartType": 6, 22 | "visualEffectType": 4, 23 | "texture": { 24 | "fileName": "growserver_item_wing.rttex", 25 | "pathAsset": "game/growserver_item_wing.png", 26 | "pathResult": "growserver_item_wing.rttex" 27 | } 28 | }, 29 | "storePath": "game" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": "." 24 | }, 25 | "exclude": ["node_modules", "./node_modules", "./node_modules/**/*"], 26 | } 27 | -------------------------------------------------------------------------------- /packages/const/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": "." 24 | }, 25 | "exclude": ["node_modules", "./node_modules", "./node_modules/**/*"], 26 | } 27 | -------------------------------------------------------------------------------- /packages/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": "." 24 | }, 25 | "exclude": ["node_modules", "./node_modules", "./node_modules/**/*"], 26 | } 27 | -------------------------------------------------------------------------------- /packages/logger/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": "." 24 | }, 25 | "exclude": ["node_modules", "./node_modules", "./node_modules/**/*"], 26 | } 27 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": "." 24 | }, 25 | "exclude": ["node_modules", "./node_modules", "./node_modules/**/*"], 26 | } 27 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": "." 24 | }, 25 | "exclude": ["node_modules", "./node_modules", "./node_modules/**/*"], 26 | } 27 | -------------------------------------------------------------------------------- /apps/server/src/network/tanks/ItemActiveObjectReq.ts: -------------------------------------------------------------------------------- 1 | import { TankPacket } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { World } from "../../core/World"; 5 | 6 | export class ItemActiveObjectReq { 7 | private pos: number; 8 | 9 | constructor( 10 | public base: Base, 11 | public peer: Peer, 12 | public tank: TankPacket, 13 | public world: World, 14 | ) { 15 | this.pos = 16 | (this.tank.data?.xPunch as number) + 17 | (this.tank.data?.yPunch as number) * this.world.data.width; 18 | } 19 | 20 | public async execute() { 21 | // Prevent item pickup in ghost mode 22 | if (this.peer.data.state.isGhost) return; 23 | 24 | const dropped = this.world.data.dropped?.items.find( 25 | (i) => i.uid === this.tank.data?.info, 26 | ); 27 | if (dropped) this.world.collect(this.peer, dropped.uid); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/logon/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": ".", 24 | "jsx": "react-jsx", 25 | "jsxImportSource": "hono/jsx" 26 | }, 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/login-page/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun"], 23 | "rootDir": ".", 24 | "jsx": "react-jsx", 25 | "jsxImportSource": "hono/jsx" 26 | }, 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/Ping.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export default class Ping extends Command { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public text: string, 12 | public args: string[], 13 | ) { 14 | super(base, peer, text, args); 15 | this.opt = { 16 | command: ["ping", "pong"], 17 | description: "Ping pong", 18 | cooldown: 5, 19 | ratelimit: 1, 20 | category: "`oBasic", 21 | usage: "/ping", 22 | example: ["/ping"], 23 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 24 | }; 25 | } 26 | 27 | public async execute(): Promise { 28 | this.peer.send(Variant.from("OnConsoleMessage", "Pong :>")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/server/src/network/tanks/AppCheckResponsePack.ts: -------------------------------------------------------------------------------- 1 | import { TankPacket } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { World } from "../../core/World"; 5 | 6 | export class AppCheckResponsePack { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public tank: TankPacket, 11 | public world: World, 12 | ) {} 13 | 14 | public async execute() { 15 | // Client validation 16 | console.log("Executing AppCheckResponsePack..."); 17 | if (this.tank.data?.type === 24) { 18 | console.log("Valid APP_CHECK_RESPONSE packet received."); 19 | if (this.peer.isValid()) { 20 | console.log("Peer is valid."); 21 | } else { 22 | console.log("Peer is invalid. Disconnecting..."); 23 | this.peer.disconnect(); 24 | } 25 | } else { 26 | console.log("Invalid APP_CHECK_RESPONSE packet received."); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/server/src/network/tanks/SetIconState.ts: -------------------------------------------------------------------------------- 1 | import { TankPacket } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { World } from "../../core/World"; 5 | import { TileData } from "@growserver/types"; 6 | 7 | export class SetIconState { 8 | private pos: number; 9 | private block: TileData; 10 | 11 | constructor( 12 | public base: Base, 13 | public peer: Peer, 14 | public tank: TankPacket, 15 | public world: World, 16 | ) { 17 | this.pos = 18 | (this.tank.data?.xPunch as number) + 19 | (this.tank.data?.yPunch as number) * this.world.data.width; 20 | this.block = this.world.data.blocks[this.pos]; 21 | } 22 | 23 | public async execute() { 24 | this.tank.data!.state = this.peer.data?.rotatedLeft ? 16 : 0; 25 | 26 | const world = this.peer.currentWorld(); 27 | if (world) { 28 | world.every((p) => { 29 | p.send(this.tank); 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "tsx", 6 | "type": "node", 7 | "request": "launch", 8 | // Debug current file in VSCode 9 | "program": "${workspaceFolder}/apps/server/src/app.ts", 10 | /* 11 | * Path to tsx binary 12 | * Assuming locally installed 13 | */ 14 | "runtimeExecutable": "pnpm", 15 | "runtimeArgs": [ 16 | "run", 17 | "dev" 18 | ], 19 | /* 20 | * Open terminal when debugging starts (Optional) 21 | * Useful to see console.logs 22 | */ 23 | "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen", 25 | // Files to exclude from debugger (e.g. call stack) 26 | "skipFiles": [ 27 | // Node.js internal core modules 28 | "/**", 29 | // Ignore all dependencies (optional) 30 | "${workspaceFolder}/node_modules/**" 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /apps/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "lib": ["ES2015", "ES2016", "ES2017", "ES2018", "ES2019", "ES2020", "ES2021", "ES2022"], 5 | "module": "CommonJS", 6 | "target": "ES2022", 7 | "noFallthroughCasesInSwitch": true, 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "moduleResolution": "node", 13 | "allowUnusedLabels": false, 14 | "allowUnreachableCode": false, 15 | "checkJs": true, 16 | "strictPropertyInitialization": true, 17 | "strictBindCallApply": true, 18 | "declaration": true, 19 | "noEmitOnError": true, 20 | "allowSyntheticDefaultImports": true, 21 | "resolveJsonModule": true, 22 | "types": ["@types/bun", "node"], 23 | "rootDir": "." 24 | }, 25 | "exclude": ["node_modules", "./node_modules", "./node_modules/**/*"], 26 | "include": ["src/**/*", "src/types/**/*", "scripts/**/*", "drizzle.config.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/FindItemEnd.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { Variant } from "growtopia.js"; 5 | 6 | export class FindItemEnd { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public action: NonEmptyObject<{ 11 | dialog_name: string; 12 | buttonClicked: string; 13 | }>, 14 | ) {} 15 | 16 | public async execute(): Promise { 17 | if (!this.action.dialog_name || !this.action.buttonClicked) return; 18 | const itemID = parseInt(this.action.buttonClicked); 19 | 20 | this.peer.data?.inventory?.items.push({ id: itemID, amount: 200 }); 21 | this.peer.send( 22 | Variant.from( 23 | "OnConsoleMessage", 24 | `Added \`6${this.base.items.metadata.items.get(itemID.toString())?.name}\`\` to your inventory.`, 25 | ), 26 | ); 27 | this.peer.inventory(); 28 | this.peer.saveToCache(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/logger/index.ts: -------------------------------------------------------------------------------- 1 | import pino from "pino"; 2 | import { config } from "@growserver/config"; 3 | const isDevelopment = process.env.NODE_ENV !== "production"; 4 | 5 | export const logger = pino({ 6 | level: config.server.logLevel, 7 | transport: isDevelopment 8 | ? { 9 | target: "pino-pretty", 10 | options: { 11 | colorize: true, 12 | translateTime: "SYS:standard", 13 | ignore: "pid,hostname", 14 | }, 15 | } 16 | : undefined, 17 | formatters: { 18 | level: (label) => { 19 | return { level: label.toUpperCase() }; 20 | }, 21 | }, 22 | }); 23 | 24 | export const createLogger = (name: string) => { 25 | return logger.child({ name }); 26 | }; 27 | 28 | export const debug = logger.debug.bind(logger); 29 | export const info = logger.info.bind(logger); 30 | export const warn = logger.warn.bind(logger); 31 | export const error = logger.error.bind(logger); 32 | export const fatal = logger.fatal.bind(logger); 33 | 34 | export default logger; 35 | -------------------------------------------------------------------------------- /apps/server/src/network/Action.ts: -------------------------------------------------------------------------------- 1 | import { Peer } from "../core/Peer"; 2 | import { Base } from "../core/Base"; 3 | import { parseAction } from "@growserver/utils"; 4 | import { ActionMap } from "./actions/index"; 5 | import logger from "@growserver/logger"; 6 | 7 | export class IActionPacket { 8 | public obj: Record; 9 | 10 | constructor( 11 | public base: Base, 12 | public peer: Peer, 13 | public chunk: Buffer, 14 | ) { 15 | this.obj = parseAction(chunk); 16 | } 17 | 18 | public async execute() { 19 | if (!this.obj.action) return; 20 | logger.debug(`Receive action packet:\n ${this.obj}`); 21 | 22 | const actionType = this.obj.action; 23 | 24 | try { 25 | const Class = ActionMap[actionType]; 26 | 27 | if (!Class) 28 | throw new Error(`No Action class found with action name ${actionType}`); 29 | 30 | const action = new Class(this.base, this.peer); 31 | await action.execute(this.obj); 32 | } catch (e) { 33 | logger.warn(e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/logon/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logon", 3 | "scripts": { 4 | "bun": "cross-env RUNTIME_ENV=bun bun run --bun", 5 | "node": "cross-env RUNTIME_ENV=node node", 6 | "tsx": "cross-env RUNTIME_ENV=node tsx", 7 | "dev": "nr tsx watch src/index.ts || nr bun src/index.ts", 8 | "setup": "nr tsx scripts/setup.ts || nr bun scripts/setup.ts", 9 | "clean": "rimraf .cache dist", 10 | "build": "tsc", 11 | "start": "node dist/index.js" 12 | }, 13 | "dependencies": { 14 | "@antfu/ni": "^25.0.0", 15 | "@growserver/config": "workspace:*", 16 | "@growserver/const": "workspace:*", 17 | "@growserver/db": "workspace:*", 18 | "@growserver/logger": "workspace:*", 19 | "@growserver/types": "workspace:*", 20 | "@growserver/utils": "workspace:*", 21 | "@hono/node-server": "^1.19.6", 22 | "cross-env": "^10.0.0", 23 | "hono": "^4.10.4", 24 | "rimraf": "^6.1.0" 25 | }, 26 | "devDependencies": { 27 | "@types/bun": "^1.3.2", 28 | "@types/node": "^20.11.17", 29 | "tsx": "^4.7.1", 30 | "typescript": "^5.8.3" 31 | } 32 | } -------------------------------------------------------------------------------- /packages/db/shared/schemas/World.ts: -------------------------------------------------------------------------------- 1 | import { InferSelectModel, sql } from "drizzle-orm"; 2 | import { text, integer, pgTable, serial } from "drizzle-orm/pg-core"; 3 | import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 4 | 5 | export const worlds = pgTable("worlds", { 6 | id: serial("id").primaryKey(), 7 | name: text("name").notNull(), 8 | ownedBy: integer("ownedBy"), 9 | // owner: blob("owner", { mode: "buffer" }), 10 | width: integer("width").notNull(), 11 | height: integer("height").notNull(), 12 | blocks: text("blocks"), 13 | dropped: text("dropped"), 14 | weather_id: integer("weather_id").default(41), 15 | created_at: text("created_at").default(sql`(current_timestamp)`), 16 | updated_at: text("updated_at").default(sql`(current_timestamp)`), 17 | worldlock_index: integer("worldlock_index"), 18 | // minimum_level: integer("minimum_level").default(1) 19 | }); 20 | 21 | export type Worlds = InferSelectModel; 22 | export const insertWorldSchema = createInsertSchema(worlds); 23 | export const selectWorldSchema = createSelectSchema(worlds); 24 | -------------------------------------------------------------------------------- /packages/db/shared/schemas/Player.ts: -------------------------------------------------------------------------------- 1 | import { InferSelectModel, sql } from "drizzle-orm"; 2 | import { text, integer, pgTable, serial } from "drizzle-orm/pg-core"; 3 | import { createInsertSchema, createSelectSchema } from "drizzle-zod"; 4 | 5 | export const players = pgTable("players", { 6 | id: serial("id").primaryKey(), 7 | name: text("name").notNull().unique(), 8 | display_name: text("display_name").notNull(), 9 | password: text("password").notNull(), 10 | role: text("role").notNull(), 11 | gems: integer("gems").default(0), 12 | level: integer("level").default(0), 13 | exp: integer("exp").default(0), 14 | clothing: text("clothing"), 15 | inventory: text("inventory"), 16 | last_visited_worlds: text("last_visited_worlds"), 17 | created_at: text("created_at").default(sql`(current_timestamp)`), 18 | updated_at: text("updated_at").default(sql`(current_timestamp)`), 19 | heart_monitors: text("heart_monitors").notNull(), 20 | }); 21 | 22 | export type Players = InferSelectModel; 23 | export const insertUserSchema = createInsertSchema(players); 24 | export const selectUserSchema = createSelectSchema(players); 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 JadlionHD 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/No.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class No extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["no"], 16 | description: "no", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/no", 21 | example: ["/no"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Dab.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Dab extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["dab"], 16 | description: "dab", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/dab", 21 | example: ["/dab"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Omg.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Omg extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["omg"], 16 | description: "omg", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/omg", 21 | example: ["/omg"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Shy.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Shy extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["shy"], 16 | description: "shy", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/shy", 21 | example: ["/shy"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Smh.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Smh extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["smh"], 16 | description: "smh", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/smh", 21 | example: ["/smh"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Yes.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Yes extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["yes"], 16 | description: "yes", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/yes", 21 | example: ["/yes"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/JoinRequest.ts: -------------------------------------------------------------------------------- 1 | import { Variant } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { type NonEmptyObject } from "type-fest"; 5 | 6 | export class JoinRequest { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | ) {} 11 | 12 | public async execute( 13 | action: NonEmptyObject>, 14 | ): Promise { 15 | const worldName = action.name || ""; 16 | if (worldName.length <= 0) { 17 | this.peer.send( 18 | Variant.from("OnFailedToEnterWorld", 1), 19 | Variant.from("OnConsoleMessage", "That world name is uhh `9empty``"), 20 | ); 21 | return; 22 | } 23 | if (worldName.match(/\W+|_|EXIT/gi)) { 24 | this.peer.send( 25 | Variant.from("OnFailedToEnterWorld", 1), 26 | Variant.from( 27 | "OnConsoleMessage", 28 | "That world name is too `9special`` to be entered.", 29 | ), 30 | ); 31 | return; 32 | } 33 | 34 | setTimeout(() => { 35 | this.peer.enterWorld(worldName.toUpperCase()); 36 | }, 200); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Wave.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Wave extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["wave"], 16 | description: "Wave", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/wave", 21 | example: ["/wave"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/network/tanks/index.ts: -------------------------------------------------------------------------------- 1 | import { type Class } from "type-fest"; 2 | import { TankTypes } from "@growserver/const"; 3 | import { TileChangeReq } from "./TileChangeReq"; 4 | import { Disconnect } from "./Disconnect"; 5 | import { SetIconState } from "./SetIconState"; 6 | import { State } from "./State"; 7 | import { ItemActiveObjectReq } from "./ItemActiveObjectReq"; 8 | import { TileActiveReq } from "./TileActiveReq"; 9 | import { ItemActiveReq } from "./ItemActiveReq"; 10 | import { AppCheckResponsePack } from "./AppCheckResponsePack"; 11 | 12 | export const TankMap: Record< 13 | number, 14 | Class<{ 15 | execute: () => Promise; 16 | }> 17 | > = { 18 | [TankTypes.TILE_CHANGE_REQUEST]: TileChangeReq, 19 | [TankTypes.DISCONNECT]: Disconnect, 20 | [TankTypes.SET_ICON_STATE]: SetIconState, 21 | [TankTypes.STATE]: State, 22 | [TankTypes.ITEM_ACTIVATE_OBJECT_REQUEST]: ItemActiveObjectReq, 23 | [TankTypes.ITEM_ACTIVATE_REQUEST]: ItemActiveReq, 24 | [TankTypes.TILE_ACTIVATE_REQUEST]: TileActiveReq, 25 | [TankTypes.APP_CHECK_RESPONSE]: AppCheckResponsePack, 26 | }; 27 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Cheer.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Cheer extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["cheer"], 16 | description: "cheer", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/cheer", 21 | example: ["/cheer"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Love.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Love extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["love", "kiss"], 16 | description: "Love", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/love", 21 | example: ["/love"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/March.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class March extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["march"], 16 | description: "march", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/march", 21 | example: ["/march"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Sassy.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Sassy extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["sassy"], 16 | description: "sassy", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/sassy", 21 | example: ["/sassy"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Sleep.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Sleep extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["sleep"], 16 | description: "Sleep", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/sleep", 21 | example: ["/sleep"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Dance.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Dance extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["dance"], 16 | description: "Just Dance", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/dance", 21 | example: ["/dance"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Grumpy.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Grumpy extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["grumpy"], 16 | description: "grumpy", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/grumpy", 21 | example: ["/grumpy"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Shrug.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Shrug extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["shrug", "idk"], 16 | description: "shrug", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/idk", 21 | example: ["/shrug"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Troll.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export default class Troll extends Command { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public text: string, 12 | public args: string[], 13 | ) { 14 | super(base, peer, text, args); 15 | this.opt = { 16 | command: ["troll"], 17 | description: "troll", 18 | cooldown: 0, 19 | ratelimit: 1, 20 | category: "Emote", 21 | usage: "/troll", 22 | example: ["/troll"], 23 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 24 | }; 25 | } 26 | public async execute(): Promise { 27 | const world = this.peer.currentWorld(); 28 | const varlist = Variant.from( 29 | { netID: this.peer.data.netID }, 30 | "OnAction", 31 | this.opt.usage, 32 | ); 33 | 34 | if (world) { 35 | world.every((p) => { 36 | p.send(varlist); 37 | }); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/login-page/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "login-page", 3 | "scripts": { 4 | "bun": "cross-env RUNTIME_ENV=bun bun run --bun", 5 | "node": "cross-env RUNTIME_ENV=node node", 6 | "tsx": "cross-env RUNTIME_ENV=node tsx", 7 | "dev": "nr tsx watch src/index.ts || nr bun src/index.ts", 8 | "setup": "nr tsx scripts/setup.ts || nr bun scripts/setup.ts", 9 | "clean": "rimraf .cache dist", 10 | "build": "tsc", 11 | "start": "node dist/index.js" 12 | }, 13 | "dependencies": { 14 | "@antfu/ni": "^25.0.0", 15 | "@growserver/config": "workspace:*", 16 | "@growserver/const": "workspace:*", 17 | "@growserver/db": "workspace:*", 18 | "@growserver/logger": "workspace:*", 19 | "@growserver/types": "workspace:*", 20 | "@growserver/utils": "workspace:*", 21 | "@hono/node-server": "^1.19.6", 22 | "bcryptjs": "^3.0.2", 23 | "cross-env": "^10.0.0", 24 | "hono": "^4.10.4", 25 | "jsonwebtoken": "^9.0.2", 26 | "rimraf": "^6.1.0" 27 | }, 28 | "devDependencies": { 29 | "@types/bun": "^1.3.2", 30 | "@types/jsonwebtoken": "^9.0.10", 31 | "@types/node": "^20.11.17", 32 | "tsx": "^4.7.1", 33 | "typescript": "^5.8.3" 34 | } 35 | } -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Dance2.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Dance2 extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["dance2"], 16 | description: "Just Dance2", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/dance2", 21 | example: ["/dance2"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Facepalm.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Facepalm extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["facepalm", "fp"], 16 | description: "Facepalm", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/fp", 21 | example: ["/fp"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Furious.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Furious extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["furious"], 16 | description: "furious", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/furious", 21 | example: ["/furious"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Rolleyes.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Rolleyes extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["rolleyes", "eyeroll"], 16 | description: "rolleyes", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/rolleyes", 21 | example: ["/rolleyes"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Foldarms.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class foldarms extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["foldarms", "fa", "stubborn", "fold"], 16 | description: "foldarms", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/fold", 21 | example: ["/fold"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | 33 | if (world) { 34 | world.every((p) => { 35 | p.send(varlist); 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@growserver/db", 3 | "version": "0.0.0", 4 | "description": "GrowServer's database management", 5 | "main": "./index.ts", 6 | "scripts": { 7 | "generate": "drizzle-kit push --config=drizzle.config.ts", 8 | "db:seed": "nr tsx scripts/seeds.ts || nr bun scripts/seeds.ts", 9 | "db:studio": "drizzle-kit studio" 10 | }, 11 | "keywords": [], 12 | "author": "JadlionHD ", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@antfu/ni": "^25.0.0", 16 | "@growserver/config": "workspace:*", 17 | "@growserver/const": "workspace:*", 18 | "@growserver/types": "workspace:*", 19 | "@growserver/utils": "workspace:*", 20 | "bcryptjs": "^3.0.2", 21 | "dotenv": "^17.2.2", 22 | "drizzle-orm": "^0.44.5", 23 | "drizzle-zod": "^0.8.3", 24 | "nanoid": "5.1.5", 25 | "postgres": "^3.4.7", 26 | "rimraf": "^6.0.1", 27 | "tsx": "^4.20.5", 28 | "zod": "^4.1.5" 29 | }, 30 | "devDependencies": { 31 | "@types/bun": "^1.2.21", 32 | "@types/node": "^24.3.1", 33 | "drizzle-kit": "^0.31.4", 34 | "typescript": "^5.9.2" 35 | }, 36 | "engines": { 37 | "node": ">=18.0.0", 38 | "npm": ">=7.0.0" 39 | } 40 | } -------------------------------------------------------------------------------- /apps/server/scripts/item-info/build.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from "fs/promises"; 2 | import { ItemsDat } from "grow-items"; 3 | import { join } from "path"; 4 | import { Scraper } from "./scraper"; 5 | import { Parser } from "./parser"; 6 | import { downloadItemsDat, getLatestItemsDatName } from "@growserver/utils"; 7 | import logger from "@growserver/logger"; 8 | 9 | __dirname = process.cwd(); 10 | 11 | export async function buildItemsInfo() { 12 | const itemsDatName = await getLatestItemsDatName(); 13 | await downloadItemsDat(itemsDatName); 14 | 15 | const ITEMS_DAT_PATH = join( 16 | __dirname, 17 | ".cache", 18 | "growtopia", 19 | "dat", 20 | itemsDatName, 21 | ); 22 | 23 | const file = await readFile(ITEMS_DAT_PATH); 24 | const itemsdat = new ItemsDat(Array.from(file)); 25 | await itemsdat.decode(); 26 | 27 | const allItems = Array.from(itemsdat.meta.items.values()); 28 | const scraper = new Scraper(allItems); 29 | 30 | const itemPages = await scraper.getItemPages(); 31 | 32 | const parser = new Parser(itemPages, allItems); 33 | const items = await parser.pagesToItems(); 34 | 35 | logger.info("Writing ItemsInfo file into ./assets/items_info_new.json"); 36 | await writeFile("./assets/items_info_new.json", JSON.stringify(items)); 37 | } 38 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/SaveServer.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export default class SaveServer extends Command { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public text: string, 12 | public args: string[], 13 | ) { 14 | super(base, peer, text, args); 15 | this.opt = { 16 | command: ["saveserver", "save"], 17 | description: "Save the server into database", 18 | cooldown: 5, 19 | ratelimit: 1, 20 | category: "`bDev", 21 | usage: "/saveserver", 22 | example: ["/saveserver"], 23 | permission: [ROLE.DEVELOPER], 24 | }; 25 | } 26 | 27 | public async execute(): Promise { 28 | this.peer.send( 29 | Variant.from("On%Message", "Saving all worlds & players..."), 30 | ); 31 | 32 | // Use the existing base instance instead of creating a new one 33 | await this.base.saveAll(false); 34 | 35 | this.peer.send( 36 | Variant.from( 37 | "OnConsoleMessage", 38 | "Successfully saved all worlds & players", 39 | ), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /apps/server/src/network/Tank.ts: -------------------------------------------------------------------------------- 1 | import { Peer } from "../core/Peer"; 2 | import { Base } from "../core/Base"; 3 | import { TankPacket } from "growtopia.js"; 4 | import { TankTypes } from "@growserver/const"; 5 | import { World } from "../core/World"; 6 | import { TankMap } from "./tanks/index"; 7 | import logger from "@growserver/logger"; 8 | 9 | export class ITankPacket { 10 | public tank; 11 | 12 | constructor( 13 | public base: Base, 14 | public peer: Peer, 15 | public chunk: Buffer, 16 | ) { 17 | this.tank = TankPacket.fromBuffer(chunk); 18 | } 19 | 20 | public async execute() { 21 | const tankType = this.tank.data?.type as number; 22 | const world = new World(this.base, this.peer.data.world); 23 | 24 | logger.debug( 25 | `[DEBUG] Receive tank packet of ${TankTypes[tankType]}: ${this.tank.parse()?.toString("hex")}`, 26 | ); 27 | 28 | try { 29 | const type = this.tank.data?.type as number; 30 | const Class = TankMap[type]; 31 | 32 | if (!Class) 33 | throw new Error( 34 | `No TankPacket class found with type ${TankTypes[type]} (${type})`, 35 | ); 36 | 37 | const tnk = new Class(this.base, this.peer, this.tank, world); 38 | await tnk.execute(); 39 | } catch (e) { 40 | logger.warn(e); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /apps/server/scripts/update-role.ts: -------------------------------------------------------------------------------- 1 | // DISABLED FOR NOW 2 | 3 | // import { drizzle } from "drizzle-orm/libsql"; 4 | // import { createClient } from "@libsql/client"; 5 | // import { players } from "../src/database/schemas/Player"; 6 | // import { eq } from "drizzle-orm"; 7 | // import consola from "consola"; 8 | 9 | // const username = process.argv[2]; 10 | // const roleValue = process.argv[3] || "1"; // Default to DEVELOPER 11 | 12 | // if (!username) { 13 | // consola.error("Please provide a username"); 14 | // consola.info("Usage: pnpm tsx scripts/update-role.ts [role]"); 15 | // consola.info("Roles: 1=DEVELOPER, 2=BASIC, 3=SUPPORTER"); 16 | // process.exit(1); 17 | // } 18 | 19 | // async function updateRole() { 20 | // const sqlite = createClient({ 21 | // url: `file:data/data.db` 22 | // }); 23 | // const db = drizzle(sqlite, { logger: false }); 24 | 25 | // try { 26 | // const result = await db 27 | // .update(players) 28 | // .set({ role: roleValue }) 29 | // .where(eq(players.name, username)); 30 | 31 | // consola.success(`Role updated to ${roleValue} for user: ${username}`); 32 | // process.exit(0); 33 | // } catch (error) { 34 | // consola.error("Failed to update role:", error); 35 | // process.exit(1); 36 | // } 37 | // } 38 | 39 | // updateRole(); 40 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Sad.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Sad extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["sad"], 16 | description: "sad", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/sad", 21 | example: ["/sad"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | const talkBubbleVarlist = Variant.from( 33 | "OnTalkBubble", 34 | this.peer.data.netID, 35 | "CP:0_PL:0_OID:_:(", 36 | 0, 37 | ); 38 | if (world) { 39 | world.every((p) => { 40 | p.send(varlist, talkBubbleVarlist); 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/emotes/Wink.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../../Command"; 2 | import { Base } from "../../../core/Base"; 3 | import { Peer } from "../../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | export default class Wink extends Command { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public text: string, 11 | public args: string[], 12 | ) { 13 | super(base, peer, text, args); 14 | this.opt = { 15 | command: ["wink"], 16 | description: "wink", 17 | cooldown: 0, 18 | ratelimit: 1, 19 | category: "Emote", 20 | usage: "/wink", 21 | example: ["/wink"], 22 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 23 | }; 24 | } 25 | public async execute(): Promise { 26 | const world = this.peer.currentWorld(); 27 | const varlist = Variant.from( 28 | { netID: this.peer.data.netID }, 29 | "OnAction", 30 | this.opt.usage, 31 | ); 32 | const talkBubbleVarlist = Variant.from( 33 | "OnTalkBubble", 34 | this.peer.data.netID, 35 | "CP:0_PL:0_OID:_;)", 36 | 0, 37 | ); 38 | if (world) { 39 | world.every((p) => { 40 | p.send(varlist, talkBubbleVarlist); 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/logon/assets/ssl/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDezCCAmMCFEwc7lnwQW3YD2GTvN9sOzJeyp9/MA0GCSqGSIb3DQEBCwUAMHox 3 | CzAJBgNVBAYTAklEMRAwDgYDVQQIDAdKYWthcnRhMRAwDgYDVQQHDAdKYWthcnRh 4 | MRQwEgYDVQQKDAtHcm93dG9waWFJTzEUMBIGA1UECwwLR3Jvd3RvcGlhSU8xGzAZ 5 | BgNVBAMMEnd3dy5ncm93dG9waWExLmNvbTAeFw0yMjEwMDkwOTAwMjRaFw0yMzEw 6 | MDkwOTAwMjRaMHoxCzAJBgNVBAYTAklEMRAwDgYDVQQIDAdKYWthcnRhMRAwDgYD 7 | VQQHDAdKYWthcnRhMRQwEgYDVQQKDAtHcm93dG9waWFJTzEUMBIGA1UECwwLR3Jv 8 | d3RvcGlhSU8xGzAZBgNVBAMMEnd3dy5ncm93dG9waWExLmNvbTCCASIwDQYJKoZI 9 | hvcNAQEBBQADggEPADCCAQoCggEBAKwGwqNCrChP6ZtQuayR7Ntr8koAlwg1terj 10 | A3PQgSllGCLDJtAIA+iXNKL09okIzV0rd8HoBCles8l3xBCd7dsgrghpPcpC1q/M 11 | EhA39Eq0shODm7I5YGDoI9CWOPnutwmQ+H3SjfmOiFFX0lIldA6EL4yvtWyXDGGn 12 | QeBOZOYb114KXm1mqpN/YbYA51IE0/dJqNC5Lf5Wa45Hn2oTA+fUKCyIMSPjk8nF 13 | rcYqd+cdCnuEC4oWqhbpR+jE76X/ZSFwWSVmdAflbxNAtBH9xm8lT/QL5tD/2DMw 14 | rVs9HGvro7uW8FBmdSL4btSZYU0DdNURJAaIC8bpOGVVPbp34McCAwEAATANBgkq 15 | hkiG9w0BAQsFAAOCAQEAMWzTZ5iS0sOA3YQ3gisqCAtPjLPkeGVh9o11iCRR8rS/ 16 | Ld0ufgIatGq3+iCZg7ihhGXoO6mQ8nzB7h1hQefeh/nuFwaZrqgAN3w5Sfm7Luh9 17 | JyLXRKG/QrTW4jcUy4DF7wIBK3mlLQM2czKXO+PlOX4zlj7fSUMAgQr8ikTZpLiF 18 | Y4r/Q3Gj8n7ocrBfDo5S1/ufUjUd+0A7Z7XCGz0V+k6qAslgfoAwxNwedKwhFGhp 19 | fBXKiBrDNrB+xkbDGq2i+8wecbjo1ksjVBEcnkb9/ReQII7ukmwouFQAbxv6zO5f 20 | 61a6wKf7Rz24Fxs5TxoqXOPKB5yxwJ2lDjpkz2MlKw== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/Search.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | import { DialogBuilder } from "@growserver/utils"; 7 | 8 | export default class Search extends Command { 9 | constructor( 10 | public base: Base, 11 | public peer: Peer, 12 | public text: string, 13 | public args: string[], 14 | ) { 15 | super(base, peer, text, args); 16 | this.opt = { 17 | command: ["search"], 18 | description: "Search items with searchable item list", 19 | cooldown: 5, 20 | ratelimit: 5, 21 | category: "`oBasic", 22 | usage: "/search", 23 | example: ["/search"], 24 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 25 | }; 26 | } 27 | 28 | public async execute(): Promise { 29 | const dialog = new DialogBuilder() 30 | .defaultColor() 31 | .addInputBox("n", "Search: ", "", 26) 32 | .raw( 33 | "add_searchable_item_list||sourceType:allItems;listType:iconWithCustomLabel;resultLimit:50|n|\n", 34 | ) 35 | .addQuickExit() 36 | .endDialog("search_item", "", "") 37 | .str(); 38 | 39 | this.peer.send(Variant.from("OnDialogRequest", dialog)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/Wrench.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { DialogBuilder } from "@growserver/utils"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export class Wrench { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | ) {} 12 | 13 | private InfoDialog(): DialogBuilder { 14 | // for now will put basic info 15 | return new DialogBuilder() 16 | .defaultColor() 17 | .addLabelWithIcon( 18 | `${this.peer.data.displayName}\`w's Information (uid: ${this.peer.data.userID})`, 19 | 32, 20 | "big", 21 | ) 22 | .addTextBox(`Player Infomation`) 23 | .addSmallText( 24 | `Level: ${this.peer.data.level || 1} (${this.peer.data.exp}/${this.peer.calculateRequiredLevelXp(this.peer.data.level)})`, 25 | ) 26 | .addSmallText(`Gems: ${this.peer.data.gems || 0}`) 27 | .addSmallText(`NetID: ${this.peer.data.netID}`); 28 | } 29 | 30 | public async execute( 31 | _action: NonEmptyObject>, 32 | ): Promise { 33 | const dialog = this.InfoDialog() 34 | .endDialog("wrench_end", "Cancel", "OK") 35 | .addQuickExit() 36 | .str(); 37 | 38 | this.peer.send(Variant.from("OnDialogRequest", dialog)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apps/server/src/events/Disconnect.ts: -------------------------------------------------------------------------------- 1 | import { Base } from "../core/Base"; 2 | import { World } from "../core/World"; 3 | import { tileFrom, tileUpdateMultiple } from "../world/tiles"; 4 | import { TileFlags } from "@growserver/const"; 5 | import { HeartMonitorTile } from "../world/tiles/HeartMonitorTile"; 6 | import logger from "@growserver/logger"; 7 | 8 | export class DisconnectListener { 9 | constructor(public base: Base) { 10 | logger.info('Listening ENet "disconnect" event'); 11 | } 12 | 13 | public run(netID: number): void { 14 | const peer = this.base.cache.peers.find((id) => id.netID == netID); 15 | if (peer && peer.heartMonitors) { 16 | peer.heartMonitors.forEach((indexes, worldName) => { 17 | const tiles = new Array(); 18 | const worldData = this.base.cache.worlds.get(worldName); 19 | 20 | if (!worldData || worldData.playerCount == 0) return; 21 | 22 | const world = new World(this.base, worldName); 23 | 24 | for (const index of indexes) { 25 | const heartMonitorTile = tileFrom( 26 | this.base, 27 | world, 28 | worldData.blocks[index], 29 | ); 30 | 31 | tiles.push(heartMonitorTile as HeartMonitorTile); 32 | } 33 | 34 | tileUpdateMultiple(world, tiles); 35 | }); 36 | } 37 | 38 | logger.info(`Peer ${netID} disconnected`); 39 | this.base.cache.peers.delete(netID); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/Drop.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { DialogBuilder } from "@growserver/utils"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export class Drop { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | ) {} 12 | 13 | public async execute( 14 | action: NonEmptyObject>, 15 | ): Promise { 16 | const itemID = parseInt(action.itemID); 17 | 18 | // Prevent dropping specific items add to the list if you want to prevent more items 19 | if (itemID === 18 || itemID === 32) { 20 | this.peer.send( 21 | Variant.from("OnConsoleMessage", "You'd be sorry if you lost that."), 22 | ); 23 | return; 24 | } 25 | 26 | const item = this.base.items.metadata.items.get(itemID.toString()); 27 | 28 | const peerItem = this.peer.data.inventory.items.find( 29 | (v) => v.id === itemID, 30 | ); 31 | const dialog = new DialogBuilder() 32 | .defaultColor() 33 | .addLabelWithIcon(`Drop ${item?.name}`, item?.id || 0, "big") 34 | .addTextBox("How many to drop?") 35 | .addInputBox("drop_count", "", peerItem?.amount, 5) 36 | .embed("itemID", itemID) 37 | .endDialog("drop_end", "Cancel", "OK") 38 | .str(); 39 | this.peer.send(Variant.from("OnDialogRequest", dialog)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/Punch.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export default class PunchID extends Command { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public text: string, 12 | public args: string[], 13 | ) { 14 | super(base, peer, text, args); 15 | this.opt = { 16 | command: ["pid", "punchid"], 17 | description: "Change your Punch ID", 18 | cooldown: 1, 19 | ratelimit: 1, 20 | category: "`bDev", 21 | usage: "/pid ", 22 | example: ["/pid 1234"], 23 | permission: [ROLE.DEVELOPER], 24 | }; 25 | } 26 | 27 | public async execute(): Promise { 28 | const newPunchID = this.args[0]; 29 | if (!newPunchID || isNaN(Number(newPunchID))) { 30 | this.peer.send( 31 | Variant.from( 32 | "OnConsoleMessage", 33 | "`4Usage: /pid (must be a number)", 34 | ), 35 | ); 36 | return; 37 | } 38 | 39 | this.peer.customPunchID = Number(newPunchID); 40 | 41 | this.peer.send( 42 | Variant.from( 43 | "OnConsoleMessage", 44 | `\`2Your Punch ID has been changed to: ${newPunchID}`, 45 | ), 46 | ); 47 | this.peer.sendState(Number(newPunchID)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/config/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { join } from "path"; 3 | import { parse } from "smol-toml"; 4 | 5 | export interface WebConfig { 6 | development: boolean; 7 | address: string; 8 | port: number; 9 | ports: number[]; 10 | loginUrl: string; 11 | cdnUrl: string; 12 | maintenance: { 13 | enable: boolean; 14 | message: string; 15 | }; 16 | tls: { 17 | key: string; 18 | cert: string; 19 | }; 20 | } 21 | 22 | export interface WebFrontendConfig { 23 | root: string; 24 | port: number; 25 | tls: { 26 | key: string; 27 | cert: string; 28 | }; 29 | } 30 | 31 | export interface ServerConfig { 32 | bypassVersionCheck: boolean; 33 | logLevel: string; 34 | } 35 | 36 | export interface Config { 37 | web: WebConfig; 38 | webFrontend: WebFrontendConfig; 39 | server: ServerConfig; 40 | } 41 | 42 | const configPath = join(__dirname, "config.toml"); 43 | const configContent = readFileSync(configPath, "utf-8"); 44 | const config = parse(configContent) as unknown as Config; 45 | const frontend = () => { 46 | return { 47 | tls: { 48 | key: readFileSync(config.webFrontend.tls.key), 49 | cert: readFileSync(config.webFrontend.tls.cert), 50 | }, 51 | }; 52 | }; 53 | const logon = () => { 54 | return { 55 | tls: { 56 | key: readFileSync(config.web.tls.key), 57 | cert: readFileSync(config.web.tls.cert), 58 | }, 59 | }; 60 | }; 61 | 62 | export { config, frontend, logon }; 63 | -------------------------------------------------------------------------------- /apps/server/src/events/Raw.ts: -------------------------------------------------------------------------------- 1 | import { PacketTypes } from "@growserver/const"; 2 | import { Base } from "../core/Base"; 3 | import { Peer } from "../core/Peer"; 4 | import { IActionPacket } from "../network/Action"; 5 | import { ITextPacket } from "../network/Text"; 6 | import { ITankPacket } from "../network/Tank"; 7 | import { Variant } from "growtopia.js"; 8 | import logger from "@growserver/logger"; 9 | 10 | export class RawListener { 11 | constructor(public base: Base) { 12 | logger.info('Listening ENet "raw" event'); 13 | } 14 | 15 | public run(netID: number, _channelID: number, chunk: Buffer): void { 16 | const peer = new Peer(this.base, netID); 17 | const type = chunk.readInt32LE(); 18 | 19 | switch (type) { 20 | case PacketTypes.STR: 21 | case PacketTypes.ACTION: { 22 | new ITextPacket(this.base, peer, chunk).execute(); 23 | new IActionPacket(this.base, peer, chunk).execute(); 24 | break; 25 | } 26 | 27 | case PacketTypes.TANK: { 28 | if (chunk.length < 60) { 29 | peer.send( 30 | Variant.from("OnConsoleMessage", "Received invalid tank packet."), 31 | ); 32 | return peer.disconnect(); 33 | } 34 | 35 | new ITankPacket(this.base, peer, chunk).execute(); 36 | break; 37 | } 38 | 39 | default: { 40 | logger.debug(`Unknown PacketType of ${type}: ${chunk.toString("hex")}`); 41 | break; 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/index.ts: -------------------------------------------------------------------------------- 1 | import type { Class } from "type-fest"; 2 | import { GazzetteEnd } from "./GazetteEnd"; 3 | import { FindItem } from "./FindItem"; 4 | import { FindItemEnd } from "./FindItemEnd"; 5 | import { SearchItem } from "./SearchItem"; 6 | import { AreaLockEdit } from "./AreaLockEdit"; 7 | import { ConfirmClearWorld } from "./ConfirmClearWorld"; 8 | import { DoorEdit } from "./DoorEdit"; 9 | import { DropEnd } from "./DropEnd"; 10 | import { SignEdit } from "./SignEdit"; 11 | import { TrashEnd } from "./TrashEnd"; 12 | import { SwitcheROOEdit } from "./SwitcheROOEdit"; 13 | import { RevokeLockAccess } from "./RevokeLockAccess"; 14 | import { DisplayBlockEdit } from "./DisplayBlockEdit"; 15 | import { DiceEdit } from "./DiceEdit"; 16 | 17 | export const DialogMap: Record< 18 | string, 19 | Class<{ 20 | execute: () => Promise; 21 | }> 22 | > = { 23 | ["gazzette_end"]: GazzetteEnd, 24 | ["find_item"]: FindItem, 25 | ["find_item_end"]: FindItemEnd, 26 | ["search_item"]: SearchItem, 27 | ["area_lock_edit"]: AreaLockEdit, 28 | ["confirm_clearworld"]: ConfirmClearWorld, 29 | ["door_edit"]: DoorEdit, 30 | ["drop_end"]: DropEnd, 31 | ["sign_edit"]: SignEdit, 32 | ["trash_end"]: TrashEnd, 33 | ["switcheroo_edit"]: SwitcheROOEdit, 34 | ["revoke_lock_access"]: RevokeLockAccess, 35 | ["displayblock_edit"]: DisplayBlockEdit, 36 | ["dice_edit"]: DiceEdit, 37 | }; 38 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject, type Class } from "type-fest"; 2 | import { RefreshItemData } from "./RefreshItemData"; 3 | import { EnterGame } from "./EnterGame"; 4 | import { QuitToExit } from "./QuitToExit"; 5 | import { Quit } from "./Quit"; 6 | import { JoinRequest } from "./JoinRequest"; 7 | import { DialogReturn } from "./DialogReturn"; 8 | import { Input } from "./Input"; 9 | import { Respawn } from "./Respawn"; 10 | import { RespawnSpike } from "./RespawnSpike"; 11 | import { Drop } from "./Drop"; 12 | import { Trash } from "./Trash"; 13 | import { Wrench } from "./Wrench"; 14 | import { StoreBuy } from "./StoreBuy"; 15 | import { StoreHandler } from "./StoreHandler"; 16 | import { Info } from "./Info"; 17 | 18 | export const ActionMap: Record< 19 | string, 20 | Class<{ 21 | execute: (action: NonEmptyObject>) => Promise; 22 | }> 23 | > = { 24 | ["refresh_item_data"]: RefreshItemData, 25 | ["enter_game"]: EnterGame, 26 | ["quit_to_exit"]: QuitToExit, 27 | ["quit"]: Quit, 28 | ["join_request"]: JoinRequest, 29 | ["dialog_return"]: DialogReturn, 30 | ["input"]: Input, 31 | ["respawn"]: Respawn, 32 | ["respawn_spike"]: RespawnSpike, 33 | ["drop"]: Drop, 34 | ["trash"]: Trash, 35 | ["wrench"]: Wrench, 36 | ["buy"]: StoreBuy, 37 | ["store"]: StoreHandler, 38 | ["info"]: Info, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/types/structures/peer.d.ts: -------------------------------------------------------------------------------- 1 | export interface PeerData { 2 | channelID: number; 3 | x?: number; 4 | y?: number; 5 | world: string; 6 | inventory: Inventory; 7 | rotatedLeft: boolean; 8 | name: string; 9 | displayName: string; 10 | netID: number; 11 | country: string; 12 | platformID?: string; 13 | userID: number; 14 | role: string; 15 | gems: number; 16 | clothing: Clothing; 17 | exp: number; 18 | level: number; 19 | lastCheckpoint?: CheckPoint; 20 | lastVisitedWorlds?: string[]; 21 | state: PeerState; 22 | heartMonitors: Map>; // A map that contains the world name as the key and the array number as the list of tile index. 23 | } 24 | 25 | export interface PeerState { 26 | mod: number; 27 | canWalkInBlocks: boolean; 28 | modsEffect: number; 29 | lava: LavaState; 30 | isGhost: boolean; 31 | } 32 | 33 | export interface LavaState { 34 | damage: number; 35 | resetStateAt: number; 36 | } 37 | 38 | export interface Inventory { 39 | max: number; 40 | items: InventoryItems[]; 41 | } 42 | 43 | export interface InventoryItems { 44 | id: number; 45 | amount: number; 46 | } 47 | 48 | export interface CheckPoint { 49 | x: number; 50 | y: number; 51 | } 52 | 53 | export interface Clothing { 54 | [key: string]: number; 55 | shirt: number; 56 | pants: number; 57 | feet: number; 58 | face: number; 59 | hand: number; 60 | back: number; 61 | hair: number; 62 | mask: number; 63 | necklace: number; 64 | ances: number; 65 | } 66 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/ClearWorld.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | import { DialogBuilder } from "@growserver/utils"; 7 | 8 | export default class ClearWorld extends Command { 9 | constructor( 10 | public base: Base, 11 | public peer: Peer, 12 | public text: string, 13 | public args: string[], 14 | ) { 15 | super(base, peer, text, args); 16 | this.opt = { 17 | command: ["clearworld"], 18 | description: "Clear a world", 19 | cooldown: 60 * 10, 20 | ratelimit: 1, 21 | category: "`oBasic", 22 | usage: "/clearworld", 23 | example: [], 24 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 25 | }; 26 | } 27 | 28 | public async execute(): Promise { 29 | const world = this.peer.currentWorld(); 30 | const ownerUID = world?.getOwnerUID(); 31 | 32 | if (ownerUID) { 33 | if (ownerUID !== this.peer.data.userID) return; 34 | const dialog = new DialogBuilder() 35 | .addLabelWithIcon("Warning", "1432", "big") 36 | .addTextBox( 37 | "This will clear everything on your world, including your building. Are you sure?", 38 | ) 39 | .endDialog("confirm_clearworld", "Nevermind", "Yes"); 40 | 41 | this.peer.send(Variant.from("OnDialogRequest", dialog.str())); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/server/scripts/item-info/template.ts: -------------------------------------------------------------------------------- 1 | import { ItemDefinition } from "grow-items"; 2 | import { Template, UnnamedParameter } from "mwparser"; 3 | 4 | export class TemplateParser { 5 | public readonly _chi = ["earth", "wind", "fire", "water"]; 6 | 7 | constructor(public items: ItemDefinition[]) {} 8 | 9 | public itemIdFromName(item_name: string) { 10 | const item = this.items.find((item) => item.name === item_name); 11 | if (item) { 12 | return Number(item.id); 13 | } else { 14 | return 0; 15 | } 16 | } 17 | 18 | public splice(t: Template) { 19 | const ingredients = t.parameters 20 | .slice(0, 2) 21 | .map((ingredient) => String(ingredient.value)); 22 | const ingredient_ids = ingredients.map((ingredient) => 23 | this.itemIdFromName(ingredient), 24 | ); 25 | 26 | return ingredient_ids; 27 | } 28 | 29 | public item(t: Template) { 30 | const desc = t.parameters[0]?.value ?? "No info."; 31 | const chi = this._chi.includes(t.parameters[1]?.value.toLowerCase()) 32 | ? t.parameters[1]?.value.toLowerCase() 33 | : ""; 34 | return [desc, chi]; 35 | } 36 | 37 | public func(t: Template) { 38 | return t.parameters[0]?.value; 39 | } 40 | 41 | public playMods(t: Template) { 42 | const mods: string[] = []; 43 | 44 | // Remove first parameter (which is the combination with rest of the parameters) 45 | t.parameters.shift(); 46 | 47 | for (const param of t.parameters) { 48 | if (param instanceof UnnamedParameter) { 49 | mods.push(param.rawValue.value); 50 | } 51 | } 52 | 53 | return mods; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/RefreshItemData.ts: -------------------------------------------------------------------------------- 1 | import { Variant, TankPacket } from "growtopia.js"; 2 | import { TankTypes } from "@growserver/const"; 3 | import { Base } from "../../core/Base"; 4 | import { Peer } from "../../core/Peer"; 5 | import { NonEmptyObject } from "type-fest"; 6 | import { deflateSync } from "zlib"; 7 | import { readFileSync } from "fs"; 8 | import { join } from "path"; 9 | 10 | export class RefreshItemData { 11 | constructor( 12 | public base: Base, 13 | public peer: Peer, 14 | ) {} 15 | 16 | public async execute( 17 | _action: NonEmptyObject>, 18 | ): Promise { 19 | // Check if platformID is "2" (macOS) and load appropriate items.dat 20 | const isMacOS = this.peer.data.platformID === "2"; 21 | 22 | let itemsContent: Buffer; 23 | 24 | if (isMacOS) { 25 | // Load macOS items.dat at runtime 26 | const datDir = join(process.cwd(), ".cache", "growtopia", "dat"); 27 | const macosItemsDatName = this.base.cdn.itemsDatName.replace( 28 | ".dat", 29 | "-osx.dat", 30 | ); 31 | itemsContent = readFileSync(join(datDir, macosItemsDatName)); 32 | } else { 33 | // Use regular items.dat already loaded in memory 34 | itemsContent = this.base.items.content; 35 | } 36 | 37 | this.peer.send( 38 | Variant.from( 39 | "OnConsoleMessage", 40 | `One moment. Updating item data${isMacOS ? " (macOS)" : ""}...`, 41 | ), 42 | TankPacket.from({ 43 | type: TankTypes.SEND_ITEM_DATABASE_DATA, 44 | info: itemsContent.length, 45 | data: () => deflateSync(itemsContent), 46 | }), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/Trash.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { DialogBuilder } from "@growserver/utils"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export class Trash { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | ) {} 12 | 13 | public async execute( 14 | action: NonEmptyObject>, 15 | ): Promise { 16 | const itemID = parseInt(action.itemID); 17 | 18 | // Prevent Trashing of Certain Items 19 | if (itemID === 18 || itemID === 32) { 20 | this.peer.send( 21 | Variant.from("OnTextOverlay", "You'd be sorry if you lost that."), 22 | ); 23 | return; 24 | } 25 | 26 | const item = this.base.items.metadata.items.find((v) => v.id === itemID); 27 | 28 | const peerItem = this.peer.data.inventory.items.find( 29 | (v) => v.id === itemID, 30 | ); 31 | 32 | if (!peerItem || peerItem.amount <= 0) { 33 | this.peer.send( 34 | Variant.from( 35 | "OnTalkBubble", 36 | this.peer.data.netID, 37 | "That item doesn't exist in your inventory", 38 | ), 39 | ); 40 | return; 41 | } 42 | 43 | const dialog = new DialogBuilder() 44 | .defaultColor() 45 | .addLabelWithIcon(`Trash ${item?.name}`, item?.id || 0, "big") 46 | .addTextBox("How many to `4destroy`o?") 47 | .addInputBox("trash_count", "", 0, 5) 48 | .embed("itemID", itemID) 49 | .endDialog("trash_end", "Cancel", "OK") 50 | .str(); 51 | this.peer.send(Variant.from("OnDialogRequest", dialog)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/SearchItem.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { Variant } from "growtopia.js"; 5 | 6 | export class SearchItem { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public action: NonEmptyObject<{ 11 | dialog_name: string; 12 | buttonClicked: string; 13 | }>, 14 | ) {} 15 | 16 | public async execute(): Promise { 17 | if (!this.action.dialog_name || !this.action.buttonClicked) return; 18 | 19 | // Parse the searchableItemListButton format: searchableItemListButton_{itemID}_{index}_{param} 20 | // Example: searchableItemListButton_14992_0_-1 21 | let itemID: number; 22 | 23 | if (this.action.buttonClicked.startsWith("searchableItemListButton_")) { 24 | const parts = this.action.buttonClicked.split("_"); 25 | if (parts.length >= 2) { 26 | itemID = parseInt(parts[1]); 27 | } else { 28 | return; 29 | } 30 | } else { 31 | itemID = parseInt(this.action.buttonClicked); 32 | } 33 | 34 | // Check if valid item 35 | const item = this.base.items.metadata.items.get(itemID.toString()); 36 | if (!item) { 37 | this.peer.send( 38 | Variant.from("OnConsoleMessage", "`4Error: Invalid item ID.``"), 39 | ); 40 | return; 41 | } 42 | 43 | // Add item to inventory 44 | this.peer.addItemInven(itemID, 200); 45 | this.peer.send( 46 | Variant.from( 47 | "OnConsoleMessage", 48 | `Added \`6${item.name}\`\` (200) to your inventory.`, 49 | ), 50 | ); 51 | this.peer.saveToCache(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/logon/assets/ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEArAbCo0KsKE/pm1C5rJHs22vySgCXCDW16uMDc9CBKWUYIsMm 3 | 0AgD6Jc0ovT2iQjNXSt3wegEKV6zyXfEEJ3t2yCuCGk9ykLWr8wSEDf0SrSyE4Ob 4 | sjlgYOgj0JY4+e63CZD4fdKN+Y6IUVfSUiV0DoQvjK+1bJcMYadB4E5k5hvXXgpe 5 | bWaqk39htgDnUgTT90mo0Lkt/lZrjkefahMD59QoLIgxI+OTycWtxip35x0Ke4QL 6 | ihaqFulH6MTvpf9lIXBZJWZ0B+VvE0C0Ef3GbyVP9Avm0P/YMzCtWz0ca+uju5bw 7 | UGZ1Ivhu1JlhTQN01REkBogLxuk4ZVU9unfgxwIDAQABAoIBAFFBcDaFi6QO+x0t 8 | 6EYnN2X9exiRZsWt+RvV1w+hcSEIa3ogC/k/j/kRUv+WDc10puxXV93zpeOUo/+J 9 | 3saqkmtq6El4zIL5R0cKcY3PoEiZSXQGOkjY6Tlc7W3dR1Rm+XY/T+C+z+kM9j7V 10 | 6LZ8knE45uzhIiUExAhsZKV64hX+UlC0ge6kwaF9CZC0K/UknVypNn7aX7ViQPQs 11 | etS4uohhH7pLRO3XhfIzD8XnRsj/TianzgI09NTv5sNgKaV/IhI2Vnh6FDp1MOee 12 | fgeWBxuBRyFkcYZur44ij2fDEV6Lq/FQ1oNBncgYdws0btE3FO27z1FZj0zXBsHT 13 | rkwqM0ECgYEA1yL5KJ0pyOashAejI2N8fqRmC6emiMwFd9zNUdA4b3F6GfipiVqP 14 | hIacbIYUyFtsEepXKV4EYMh29WbnPDR6pmpnc65ZvW935qTDNVqEaEcqRW//N1zj 15 | R8zMrO5PJ2d2ea18KN7CBPKoEgeqzt4f2rPV0ChF2yw5F8UMhqp9BM8CgYEAzLON 16 | K1WXSOWQyBDlFkFKwI5YIDg/voEPxDzUHxTpgTvvzd5NWEm0fL2DR7nS9ZJ0itO/ 17 | YsR51QzpHLn6LHdu6pEfYnK2NI5+C3OfWr4ObvsWxdJAmHkJ5BVlLgXEob5jJgta 18 | Qm5/rbwne9TgEQv3bo7Isw+BmOqOF58bKfsaUokCgYEAxsTqXerve04dYJNJ2F2H 19 | 3d545hXM2SFfgAJCtW9jZRv8S1ijE2PXrANPLTmopAFL1TWluHPEKcOtnUipJsf5 20 | 9h3jXU9eXJdLuY7LSVVLdqkh1iwHKpio6WLATJqWCXsfTIbMa1p8+mNUg+wPlbhG 21 | yCNVzlAXUswGJ/8Idre4cKMCgYEAjb32jn8h1nQ/oIkyWAq1/EeUdhr86KjthfCo 22 | 4SzV04rxLhg0bmH6/DUt5kih7zGOSWL+LyHlSsU51Y5h0NCSmRIMLVtJF3NjjAJv 23 | 4aGg1PBAgJJp8Co/0xONkCSmV2lBtmI+CaoB9wdGP9TTonoqxv9Psc2W64/e/DRL 24 | 1vHs9CECgYEAm9Ti2NmuJzDAeA+cSQdMJNpJiorwxCaspf29HphXyEuoXAxPSBPu 25 | Lrr9/QZm+zaNhJX3MY3Mx5K15R3IWqThqggj2KZ1U4g0sgsgHRrcavQQ0ysYAsEh 26 | 83fLmrGgemdW2J4j7KBfLV9U6UP4TbdbarbYng8dGPNNqAjw1I98bfE= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/SwitcheROOEdit.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { TileData } from "@growserver/types"; 5 | import { World } from "../../core/World"; 6 | import { Tile } from "../../world/Tile"; 7 | import { tileFrom } from "../../world/tiles"; 8 | import { LockPermission, TileFlags } from "@growserver/const"; 9 | 10 | export class SwitcheROOEdit { 11 | private world: World; 12 | private pos: number; 13 | private block: TileData; 14 | 15 | constructor( 16 | public base: Base, 17 | public peer: Peer, 18 | public action: NonEmptyObject<{ 19 | dialog_name: string; 20 | tilex: string; 21 | tiley: string; 22 | checkbox_public?: string; 23 | }>, 24 | ) { 25 | this.world = this.peer.currentWorld()!; 26 | this.pos = 27 | parseInt(this.action.tilex) + 28 | parseInt(this.action.tiley) * (this.world?.data.width as number); 29 | this.block = this.world?.data.blocks[this.pos] as TileData; 30 | } 31 | 32 | public async execute(): Promise { 33 | if ( 34 | !this.action.dialog_name || 35 | !this.action.tilex || 36 | !this.action.tiley || 37 | !this.action.checkbox_public 38 | ) 39 | return; 40 | if ( 41 | !(await this.world.hasTilePermission( 42 | this.peer.data.userID, 43 | this.block, 44 | LockPermission.BUILD, 45 | )) 46 | ) { 47 | return; 48 | } 49 | 50 | if (this.action.checkbox_public == "1") { 51 | this.block.flags |= TileFlags.PUBLIC; 52 | } else { 53 | this.block.flags &= ~TileFlags.PUBLIC; // unset PUBLIC flag 54 | } 55 | 56 | const switcheRooTile = tileFrom(this.base, this.world, this.block); 57 | this.world.every((p) => switcheRooTile.tileUpdate(p)); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/Sb.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { PacketTypes, TextPacket, Variant } from "growtopia.js"; 6 | 7 | export default class Sb extends Command { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public text: string, 12 | public args: string[], 13 | ) { 14 | super(base, peer, text, args); 15 | this.opt = { 16 | command: ["sb"], 17 | description: "Broadcast a message to everyone", 18 | cooldown: 5, 19 | ratelimit: 1, 20 | category: "`oBasic", 21 | usage: "/sb ", 22 | example: ["/sb hello"], 23 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 24 | }; 25 | } 26 | 27 | public async execute(): Promise { 28 | if (this.args.length === 0) 29 | return this.peer.send( 30 | Variant.from("OnConsoleMessage", "Message are required."), 31 | ); 32 | 33 | const world = this.peer.currentWorld(); 34 | const msg = this.args.join(" "); 35 | const jammed = world?.data.jammers?.find( 36 | (v) => v.type === "signal", 37 | )?.enabled; 38 | 39 | this.peer.every((p) => { 40 | // Send message 41 | p.send( 42 | Variant.from( 43 | "OnConsoleMessage", 44 | `CP:0_PL:4_OID:_CT:[SB]_ \`5**\`\` from \`$\`2${ 45 | this.peer.data.displayName 46 | } \`\`\`\`(in \`$${ 47 | jammed ? "`4JAMMED``" : this.peer.data.world 48 | }\`\`) ** :\`\` \`#${msg}`, 49 | ), 50 | TextPacket.from( 51 | PacketTypes.ACTION, 52 | "action|play_sfx", 53 | `file|audio/beep.wav`, 54 | `delayMS|0`, 55 | ), 56 | ); 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/logon/src/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { Hono } from "hono"; 3 | import { config, logon } from "@growserver/config"; 4 | import { createServer } from "https"; 5 | import logger from "@growserver/logger"; 6 | 7 | async function init() { 8 | const app = new Hono(); 9 | const buns = process.versions.bun ? await import("hono/bun") : undefined; 10 | 11 | app.use("*", async (ctx, next) => { 12 | const method = ctx.req.method; 13 | const path = ctx.req.path; 14 | logger.info(`[${method}] ${path}`); 15 | await next(); 16 | }); 17 | 18 | app.post("/growtopia/server_data.php", (ctx) => { 19 | let str = ""; 20 | 21 | str += `server|${config.web.address}\n`; 22 | 23 | const randPort = 24 | config.web.ports[Math.floor(Math.random() * config.web.ports.length)]; 25 | 26 | str += `port|${randPort}\nloginurl|${config.web.loginUrl}\ntype|1\n${config.web.maintenance.enable ? "maint" : "#maint"}| 27 | ${config.web.maintenance.message} 28 | \ntype2|1\nmeta|ignoremeta\nRTENDMARKERBS1001`; 29 | 30 | return ctx.body(str); 31 | }); 32 | 33 | const ssl = logon(); 34 | 35 | if (process.env.RUNTIME_ENV === "node") { 36 | serve( 37 | { 38 | fetch: app.fetch, 39 | createServer, 40 | serverOptions: { 41 | key: ssl.tls.key, 42 | cert: ssl.tls.cert, 43 | }, 44 | port: config.web.port, 45 | }, 46 | (info) => { 47 | logger.info(`Node Logon Server is running on port ${info.port}`); 48 | }, 49 | ); 50 | } else if (process.env.RUNTIME_ENV === "bun") { 51 | logger.info(`Bun Logon Server is running on port ${config.web.port}`); 52 | Bun.serve({ 53 | fetch: app.fetch, 54 | port: config.web.port, 55 | tls: { 56 | key: ssl.tls.key, 57 | cert: ssl.tls.cert, 58 | }, 59 | }); 60 | } 61 | } 62 | 63 | init(); 64 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/ConfirmClearWorld.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ActionTypes, LOCKS, ROLE } from "@growserver/const"; 5 | import { World } from "../../core/World"; 6 | 7 | export class ConfirmClearWorld { 8 | private world: World; 9 | 10 | constructor( 11 | public base: Base, 12 | public peer: Peer, 13 | public action: NonEmptyObject<{ 14 | dialog_name: string; 15 | }>, 16 | ) { 17 | this.world = this.peer.currentWorld()!; 18 | } 19 | 20 | public async execute(): Promise { 21 | if (!this.action.dialog_name) return; 22 | const ownerUID = this.world.getOwnerUID(); 23 | if (ownerUID) { 24 | if ( 25 | ownerUID !== this.peer.data.userID && 26 | this.peer.data.role != ROLE.DEVELOPER 27 | ) 28 | return; 29 | for (let i = 0; i < this.world.data.blocks.length; i++) { 30 | const b = this.world.data.blocks[i]; 31 | const itemMeta = this.base.items.metadata.items.get( 32 | (b.fg || b.bg).toString(), 33 | )!; 34 | const mLock = LOCKS.find((l) => l.id === itemMeta.id); 35 | 36 | if ( 37 | b.fg === 6 || 38 | b.fg === 8 || 39 | (!mLock && itemMeta.type === ActionTypes.LOCK) 40 | ) 41 | continue; 42 | 43 | Object.keys(b).forEach((v) => { 44 | if (v === "x" || v === "y") { 45 | // nothing 46 | } else if (v === "fg" || v === "bg") { 47 | b[v] = 0; 48 | } else { 49 | // @ts-expect-error idk this typing 50 | b[v] = undefined; 51 | } 52 | }); 53 | } 54 | 55 | const world = this.peer.currentWorld(); 56 | if (world) { 57 | world.every((p) => { 58 | p.leaveWorld(); 59 | }); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/FindItem.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { DialogBuilder } from "@growserver/utils"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export class FindItem { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public action: NonEmptyObject<{ 12 | dialog_name: string; 13 | find_item_name: string; 14 | seed_only: string; 15 | }>, 16 | ) {} 17 | 18 | public async execute(): Promise { 19 | if ( 20 | !this.action.dialog_name || 21 | !this.action.find_item_name || 22 | !this.action.seed_only 23 | ) 24 | return; 25 | const isSeed = parseInt(this.action.seed_only) ? true : false; 26 | const dialog = new DialogBuilder() 27 | .defaultColor() 28 | .addQuickExit() 29 | .addLabelWithIcon("Find the item", "6016", "big") 30 | .addSpacer("small"); 31 | 32 | const items = this.base.items.metadata.items.filter((v) => 33 | v.name?.toLowerCase().includes(this.action.find_item_name.toLowerCase()), 34 | ); 35 | items.forEach((item) => { 36 | const itemID = item.id || 0; 37 | const itemName = item.name || ""; 38 | if (isSeed) { 39 | if (itemID % 2 === 1) 40 | dialog.addButtonWithIcon( 41 | itemID, 42 | itemID, 43 | itemName, 44 | "staticBlueFrame", 45 | item.id, 46 | ); 47 | } else { 48 | if (itemID % 2 === 0) 49 | dialog.addButtonWithIcon( 50 | itemID, 51 | itemID, 52 | itemName, 53 | "staticBlueFrame", 54 | item.id, 55 | ); 56 | } 57 | }); 58 | 59 | dialog.endDialog("find_item_end", "Cancel", ""); 60 | 61 | this.peer.send(Variant.from("OnDialogRequest", dialog.str())); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/SignEdit.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { TileData } from "@growserver/types"; 5 | import { World } from "../../core/World"; 6 | import { Tile } from "../../world/Tile"; 7 | import { ItemDefinition } from "grow-items"; 8 | import { tileFrom } from "../../world/tiles"; 9 | import { ROLE } from "@growserver/const"; 10 | 11 | export class SignEdit { 12 | private world: World; 13 | private pos: number; 14 | private block: TileData; 15 | private itemMeta: ItemDefinition; 16 | 17 | constructor( 18 | public base: Base, 19 | public peer: Peer, 20 | public action: NonEmptyObject<{ 21 | dialog_name: string; 22 | tilex: string; 23 | tiley: string; 24 | label: string; 25 | }>, 26 | ) { 27 | this.world = this.peer.currentWorld()!; 28 | this.pos = 29 | parseInt(this.action.tilex) + 30 | parseInt(this.action.tiley) * (this.world?.data.width as number); 31 | this.block = this.world?.data.blocks[this.pos] as TileData; 32 | this.itemMeta = this.base.items.metadata.items.get( 33 | this.block.fg.toString(), 34 | )!; 35 | } 36 | 37 | public async execute(): Promise { 38 | if ( 39 | !this.action.dialog_name || 40 | !this.action.tilex || 41 | !this.action.tiley || 42 | !this.action.label 43 | ) 44 | return; 45 | const ownerUID = this.world.getOwnerUID(); 46 | 47 | if (ownerUID) { 48 | if ( 49 | ownerUID !== this.peer.data?.userID && 50 | this.peer.data.role == ROLE.DEVELOPER 51 | ) 52 | return; 53 | } 54 | 55 | if (!this.block.sign) return; 56 | 57 | this.block.sign = { 58 | label: this.action.label || "", 59 | }; 60 | 61 | const signTile = tileFrom(this.base, this.world, this.block); 62 | this.world.every((p) => signTile.tileUpdate(p)); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "growserver", 3 | "private": true, 4 | "version": "3.2.0", 5 | "description": "A Growtopia private server built with Node.js and Bun.js, powered by growtopia.js", 6 | "scripts": { 7 | "build": "turbo run build", 8 | "dev": "turbo run dev", 9 | "lint": "turbo run lint", 10 | "lint:fix": "turbo run lint:fix", 11 | "setup": "turbo run setup", 12 | "clean": "turbo run clean", 13 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 14 | "check-types": "turbo run check-types" 15 | }, 16 | "author": "JadlionHD ", 17 | "contributors": [ 18 | "JadlionHD (https://github.com/JadlionHD)", 19 | "badewen (https://github.com/badewen)", 20 | "GorgonUK (https://github.com/GorgonUK)", 21 | "Mikasuru (https://github.com/Mikasuru)", 22 | "dearminder (https://github.com/dearminder)", 23 | "ZTance1 (https://github.com/ZTance1)", 24 | "Phoenix-rat (https://github.com/Phoenix-rat)", 25 | "jeffryafandi (https://github.com/jeffryafandi)", 26 | "iRestartz (https://github.com/iRestartz)", 27 | "Ritshu (https://github.com/Ritshu)", 28 | "sanderDijkxhoorn (https://github.com/sanderDijkxhoorn)", 29 | "GTPSHAX (https://github.com/GTPSHAX)", 30 | "Chadgamer2023 (https://github.com/Chadgamer2023)", 31 | "KalebChristian (https://github.com/KalebChristian)", 32 | "mario-fuckan (https://github.com/mario-fuckan)", 33 | "JustPandaEver (https://github.com/JustPandaEver)", 34 | "swagesh (https://github.com/swagesh)", 35 | "Roxaro (https://github.com/Roxaro)", 36 | "Shulej (https://github.com/Shulej)", 37 | "Willi-js (https://github.com/Willi-js)", 38 | "Zeeetu (https://github.com/Zeeetu)" 39 | ], 40 | "license": "MIT", 41 | "devDependencies": { 42 | "prettier": "^3.6.2", 43 | "turbo": "^2.5.6", 44 | "typescript": "5.9.2" 45 | }, 46 | "packageManager": "pnpm@9.0.0", 47 | "engines": { 48 | "node": ">=18" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/ChangeName.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | import { World } from "../../core/World"; 7 | import { tileFrom, tileUpdateMultiple } from "../../world/tiles"; 8 | 9 | export default class ChangeName extends Command { 10 | constructor( 11 | public base: Base, 12 | public peer: Peer, 13 | public text: string, 14 | public args: string[], 15 | ) { 16 | super(base, peer, text, args); 17 | this.opt = { 18 | command: ["changename"], 19 | description: 20 | "Change your Display Name. You still can login with your current GrowID. NOTE: When using color, please terminate the color with 2 backticks ` ` ", 21 | cooldown: 5, 22 | ratelimit: 1, 23 | category: "`oBasic", 24 | usage: "/changename ", 25 | example: ["/changename MyNewName", "/changename `bC[@]nBeCoLored@Too``"], 26 | permission: [ROLE.DEVELOPER], 27 | }; 28 | } 29 | 30 | public async execute(): Promise { 31 | if (this.args.length) { 32 | await this.peer.updateDisplayName(this.args[0]); 33 | const currentWorld = this.peer.currentWorld(); 34 | 35 | if (currentWorld) { 36 | await currentWorld.every((p) => { 37 | p.send( 38 | Variant.from( 39 | { netID: this.peer.data.netID }, 40 | "OnNameChanged", 41 | this.peer.data.displayName, 42 | ), 43 | ); 44 | }); 45 | } 46 | await this.peer.saveToCache(); 47 | await this.peer.saveToDatabase(); 48 | this.peer.send( 49 | Variant.from( 50 | "OnConsoleMessage", 51 | `Your Display name has been changed to ${this.peer.data.displayName}.`, 52 | ), 53 | ); 54 | } else { 55 | this.peer.send( 56 | Variant.from("OnConsoleMessage", "`4Usage: /changename "), 57 | ); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/DropEnd.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { Variant } from "growtopia.js"; 5 | 6 | export class DropEnd { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public action: NonEmptyObject<{ 11 | dialog_name: string; 12 | drop_count: string; 13 | itemID: string; 14 | }>, 15 | ) {} 16 | 17 | public async execute(): Promise { 18 | if ( 19 | !this.action.dialog_name || 20 | !this.action.drop_count || 21 | !this.action.itemID 22 | ) 23 | return; 24 | if (!/\d/.test(this.action.drop_count) || !/\d/.test(this.action.itemID)) { 25 | this.peer.send( 26 | Variant.from( 27 | "OnTalkBubble", 28 | this.peer.data.netID, 29 | "Uh oh, thats not a valid number", 30 | ), 31 | ); 32 | return; 33 | } 34 | const itemID = parseInt(this.action.itemID); 35 | const count = parseInt(this.action.drop_count); 36 | const itemExist = this.peer.data?.inventory?.items.find( 37 | (i) => i.id === itemID, 38 | ); 39 | if (!itemExist || itemExist.amount <= 0) { 40 | this.peer.send( 41 | Variant.from( 42 | "OnTalkBubble", 43 | this.peer.data.netID, 44 | "That item, does not exist in your inventory", 45 | ), 46 | ); 47 | return; 48 | } 49 | 50 | if (count > itemExist.amount) { 51 | this.peer.send( 52 | Variant.from("OnTalkBubble", this.peer.data.netID, "Really?"), 53 | ); 54 | return; 55 | } 56 | 57 | if (count <= 0) { 58 | this.peer.send( 59 | Variant.from( 60 | "OnTalkBubble", 61 | this.peer.data.netID, 62 | "Nice try. You remind me of myself at that age.", 63 | ), 64 | ); 65 | return; 66 | } 67 | 68 | this.peer.drop(itemID, count); 69 | this.peer.removeItemInven(itemID, count); 70 | this.peer.inventory(); 71 | this.peer.sendClothes(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/DiceEdit.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { TileData } from "@growserver/types"; 5 | import { World } from "../../core/World"; 6 | import { Tile } from "../../world/Tile"; 7 | import { ItemDefinition } from "grow-items"; 8 | import { tileFrom } from "../../world/tiles"; 9 | import { LockPermission, TileFlags } from "@growserver/const"; 10 | 11 | export class DiceEdit { 12 | private world: World; 13 | private pos: number; 14 | private block: TileData; 15 | 16 | constructor( 17 | public base: Base, 18 | public peer: Peer, 19 | public action: NonEmptyObject<{ 20 | dialog_name: string; 21 | tilex: string; 22 | tiley: string; 23 | checkbox_public?: string; 24 | checkbox_silence?: string; 25 | }>, 26 | ) { 27 | this.world = this.peer.currentWorld()!; 28 | this.pos = 29 | parseInt(this.action.tilex) + 30 | parseInt(this.action.tiley) * (this.world?.data.width as number); 31 | this.block = this.world?.data.blocks[this.pos] as TileData; 32 | } 33 | 34 | public async execute(): Promise { 35 | if ( 36 | !this.action.checkbox_public || 37 | !this.action.checkbox_silence || 38 | !this.action.dialog_name || 39 | !this.action.tilex || 40 | !this.action.tiley 41 | ) 42 | return; 43 | if ( 44 | !(await this.world.hasTilePermission( 45 | this.peer.data.userID, 46 | this.block, 47 | LockPermission.BUILD, 48 | )) || 49 | !this.block.dice 50 | ) { 51 | return; 52 | } 53 | 54 | if (this.action.checkbox_public == "1") { 55 | this.block.flags |= TileFlags.PUBLIC; 56 | } else { 57 | this.block.flags &= ~TileFlags.PUBLIC; 58 | } 59 | 60 | if (this.action.checkbox_silence == "1") { 61 | this.block.flags |= TileFlags.SILENCED; 62 | } else { 63 | this.block.flags &= ~TileFlags.SILENCED; 64 | } 65 | 66 | const diceTile = tileFrom(this.base, this.world, this.block); 67 | this.world.every((p) => diceTile.tileUpdate(p)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/server/scripts/item-info/scraper.ts: -------------------------------------------------------------------------------- 1 | import { ItemDefinition } from "grow-items"; 2 | import type { ItemsPage } from "@growserver/types"; 3 | import logger from "@growserver/logger"; 4 | 5 | export class Scraper { 6 | constructor( 7 | public items: ItemDefinition[], 8 | public readonly split = 16, 9 | ) {} 10 | 11 | public async splitItems() { 12 | const sublistSize = Math.ceil(this.items.length / this.split); 13 | 14 | const sublists: ItemDefinition[] = []; 15 | 16 | for (let i = 0; i < this.items.length; i += sublistSize) { 17 | sublists.push(this.items.slice(i, i + sublistSize)); 18 | } 19 | 20 | return sublists as ItemDefinition[][]; 21 | } 22 | 23 | public async getItemPages() { 24 | const sublists = await this.splitItems(); 25 | 26 | const tasks = sublists.map((sublist, i) => 27 | this.postRequest(sublist, i + 1), 28 | ); 29 | const results = await Promise.all(tasks); 30 | 31 | logger.info("fetching items info complete."); 32 | 33 | return results as ItemsPage[]; 34 | } 35 | 36 | public async postRequest(items: ItemDefinition[], iterateNum: number) { 37 | logger.info(`ItemsInfo part ${iterateNum}, starting post request`); 38 | 39 | const names = items.map((i) => i.name); 40 | const postData = new URLSearchParams({ 41 | title: "Special:Export", 42 | pages: names.join("\n"), 43 | curonly: "1", 44 | }); 45 | 46 | const [text, status] = await this.fetchWiki(postData); 47 | 48 | if (text !== null) { 49 | logger.info(`ItemsInfo part ${iterateNum}, status: ${status}`); 50 | } else { 51 | logger.error(`ItemsInfo part ${iterateNum}, status: ${status}`); 52 | } 53 | 54 | const result = { 55 | text, 56 | items, 57 | }; 58 | return result; 59 | } 60 | 61 | private async fetchWiki(postData: URLSearchParams) { 62 | const response = await fetch( 63 | "https://growtopia.fandom.com/wiki/Special:Export", 64 | { 65 | method: "POST", 66 | body: postData, 67 | }, 68 | ); 69 | 70 | if (response.status !== 200) return [null, response.statusText]; 71 | 72 | const text = await response.text(); 73 | 74 | return [text, response.statusText]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/RevokeLockAccess.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { 5 | ActionTypes, 6 | LockPermission, 7 | LOCKS, 8 | TileFlags, 9 | } from "@growserver/const"; 10 | import { TileData } from "@growserver/types"; 11 | import { Floodfill } from "../../world/FloodFill"; 12 | import { World } from "../../core/World"; 13 | import { Tile } from "../../world/Tile"; 14 | import { ItemDefinition } from "grow-items"; 15 | import { tileFrom } from "../../world/tiles"; 16 | 17 | export class RevokeLockAccess { 18 | private world: World; 19 | private pos: number; 20 | private block: TileData; 21 | private itemMeta?: ItemDefinition; 22 | 23 | constructor( 24 | public base: Base, 25 | public peer: Peer, 26 | public action: NonEmptyObject<{ 27 | dialog_name: string; 28 | tilex: string; 29 | tiley: string; 30 | }>, 31 | ) { 32 | this.world = this.peer.currentWorld()!; 33 | this.pos = 34 | parseInt(this.action.tilex) + 35 | parseInt(this.action.tiley) * (this.world?.data.width as number); 36 | this.block = this.world?.data.blocks[this.pos] as TileData; 37 | this.itemMeta = this.base.items.metadata.items.get( 38 | this.block.fg.toString(), 39 | )!; 40 | } 41 | 42 | public async execute(): Promise { 43 | if ( 44 | !this.action.dialog_name || 45 | this.itemMeta?.type != ActionTypes.LOCK || 46 | !this.action.tilex || 47 | !this.action.tiley 48 | ) 49 | return; 50 | if (this.block.lock?.ownerUserID !== this.peer.data?.userID) { 51 | if (this.block.lock?.adminIDs?.includes(this.peer.data.userID)) { 52 | const index = this.block.lock.adminIDs.indexOf(this.peer.data.userID); 53 | if (index == -1) return; 54 | 55 | this.block.lock.adminIDs.splice(index, 1); 56 | 57 | this.world.every((p) => 58 | p.sendConsoleMessage( 59 | `${this.peer.data.displayName} removed their access from a ${this.itemMeta?.name}`, 60 | ), 61 | ); 62 | 63 | const tile = tileFrom(this.base, this.world, this.block); 64 | this.world.every((p) => tile.tileUpdate(p)); 65 | } 66 | return; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/ChangeGrowID.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | import { tileFrom, tileUpdateMultiple } from "../../world/tiles"; 7 | import { World } from "../../core/World"; 8 | 9 | export default class ChangeGrowID extends Command { 10 | constructor( 11 | public base: Base, 12 | public peer: Peer, 13 | public text: string, 14 | public args: string[], 15 | ) { 16 | super(base, peer, text, args); 17 | this.opt = { 18 | command: ["changegrowid", "changeid"], 19 | description: 20 | "Change your growid. Please remember your new GrowID before doing so, because you are going to have to use the new one to login.", 21 | cooldown: 5, 22 | ratelimit: 1, 23 | category: "`oBasic", 24 | usage: "/changegrowid ", 25 | example: ["/changegrowid MyNewGrowID"], 26 | permission: [ROLE.DEVELOPER], 27 | }; 28 | } 29 | 30 | public async execute(): Promise { 31 | if (this.args.length) { 32 | const isInCache = this.base.cache.peers.find( 33 | (p) => p.name == this.args[0], 34 | ); 35 | if (isInCache || (await this.base.database.players.get(this.args[0]))) { 36 | this.peer.send( 37 | Variant.from( 38 | "OnConsoleMessage", 39 | "`oThat GrowID has already been `4TAKEN``. Please try another one.", 40 | ), 41 | ); 42 | return; 43 | } 44 | 45 | this.peer.data.name = this.args[0]; 46 | await this.peer.updateDisplayName(); 47 | const currentWorld = this.peer.currentWorld(); 48 | if (currentWorld) { 49 | await currentWorld.every((p) => { 50 | p.send( 51 | Variant.from( 52 | { netID: this.peer.data.netID }, 53 | "OnNameChanged", 54 | this.peer.data.displayName, 55 | ), 56 | ); 57 | }); 58 | } 59 | this.peer.send( 60 | Variant.from( 61 | "OnConsoleMessage", 62 | `Your GrowID has been changed to ${this.peer.data.name}.`, 63 | ), 64 | ); 65 | } else { 66 | this.peer.send( 67 | Variant.from("OnConsoleMessage", "`4Usage: /changegrowid "), 68 | ); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /apps/server/src/network/tanks/State.ts: -------------------------------------------------------------------------------- 1 | import { TankPacket, Variant } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { World } from "../../core/World"; 5 | import { TileData } from "@growserver/types"; 6 | import { ActionTypes } from "@growserver/const"; 7 | 8 | export class State { 9 | private pos: number; 10 | private block: TileData; 11 | 12 | constructor( 13 | public base: Base, 14 | public peer: Peer, 15 | public tank: TankPacket, 16 | public world: World, 17 | ) { 18 | this.pos = 19 | (this.tank.data?.xPunch as number) + 20 | (this.tank.data?.yPunch as number) * this.world.data.width; 21 | this.block = this.world.data.blocks[this.pos]; 22 | } 23 | 24 | public async execute() { 25 | if (this.peer.data.world === "EXIT") return; 26 | this.tank.data!.netID = this.peer.data.netID; 27 | 28 | this.peer.data.x = this.tank.data?.xPos; 29 | this.peer.data.y = this.tank.data?.yPos; 30 | this.peer.data.rotatedLeft = Boolean( 31 | (this.tank.data?.state as number) & 0x10, 32 | ); 33 | 34 | this.peer.saveToCache(); 35 | 36 | const world = this.peer.currentWorld(); 37 | if (world) { 38 | world.every((p) => { 39 | p.send(this.tank); 40 | }); 41 | } 42 | 43 | this.onPlayerMove(); 44 | } 45 | 46 | private async onPlayerMove() { 47 | if ( 48 | (this.tank.data?.xPunch as number) > 0 || 49 | (this.tank.data?.yPunch as number) > 0 50 | ) 51 | return; 52 | if (this.block === undefined) return; 53 | 54 | const itemMeta = this.base.items.metadata.items.get( 55 | (this.block.fg || this.block.bg).toString(), 56 | )!; 57 | 58 | switch (itemMeta.type) { 59 | case ActionTypes.CHECKPOINT: { 60 | this.peer.send( 61 | Variant.from( 62 | { netID: this.peer.data.netID, delay: 0 }, 63 | "SetRespawnPos", 64 | this.pos, 65 | ), 66 | ); 67 | this.peer.data.lastCheckpoint = { 68 | x: Math.round((this.tank.data?.xPos as number) / 32), 69 | y: Math.round((this.tank.data?.yPos as number) / 32), 70 | }; 71 | break; 72 | } 73 | 74 | case ActionTypes.FOREGROUND: { 75 | if (itemMeta.id === 3496 || itemMeta.id === 3270) { 76 | // Steam testing 77 | } 78 | break; 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/DisplayBlockEdit.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { TileData } from "@growserver/types"; 5 | import { World } from "../../core/World"; 6 | import { Tile } from "../../world/Tile"; 7 | import { ItemDefinition } from "grow-items"; 8 | import { tileFrom } from "../../world/tiles"; 9 | import { LockPermission, ROLE, TileFlags } from "@growserver/const"; 10 | import { DisplayBlockTile } from "../../world/tiles/DisplayBlockTile"; 11 | 12 | export class DisplayBlockEdit { 13 | private world: World; 14 | private pos: number; 15 | private block: TileData; 16 | 17 | constructor( 18 | public base: Base, 19 | public peer: Peer, 20 | public action: NonEmptyObject<{ 21 | dialog_name: string; 22 | tilex: string; 23 | tiley: string; 24 | }>, 25 | ) { 26 | this.world = this.peer.currentWorld()!; 27 | this.pos = 28 | parseInt(this.action.tilex) + 29 | parseInt(this.action.tiley) * (this.world?.data.width as number); 30 | this.block = this.world?.data.blocks[this.pos] as TileData; 31 | } 32 | 33 | public async execute(): Promise { 34 | if (!this.action.dialog_name || !this.action.tilex || !this.action.tiley) 35 | return; 36 | if (!this.block.displayBlock) return; 37 | const ownerUID = this.world.getTileOwnerUID(this.block); 38 | if ( 39 | ownerUID && 40 | ownerUID != this.peer.data.userID && 41 | this.peer.data.role != ROLE.DEVELOPER 42 | ) { 43 | return; 44 | } 45 | 46 | const itemMeta = this.base.items.metadata.items.get( 47 | this.block.displayBlock!.displayedItem.toString(), 48 | )!; 49 | const dBlock = tileFrom( 50 | this.base, 51 | this.world, 52 | this.block, 53 | ) as DisplayBlockTile; 54 | 55 | if (!this.peer.canAddItemToInv(itemMeta.id!)) { 56 | this.peer.sendTextBubble( 57 | "You don't have enough space in your backpack! Free some and try again.", 58 | true, 59 | ); 60 | return; 61 | } 62 | 63 | this.peer.sendTextBubble( 64 | `You removed \`5${itemMeta!.name!}\`\` from the Display Block.`, 65 | false, 66 | ); 67 | this.peer.addItemInven(itemMeta!.id!); 68 | 69 | this.block.displayBlock = { 70 | displayedItem: 0, 71 | }; 72 | 73 | this.world.every((p) => dBlock.tileUpdate(p)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /apps/server/src/world/tiles/SwitcheROO.ts: -------------------------------------------------------------------------------- 1 | import { TankPacket, Variant } from "growtopia.js"; 2 | import { 3 | ActionTypes, 4 | BlockFlags, 5 | LockPermission, 6 | TileFlags, 7 | } from "@growserver/const"; 8 | import type { Base } from "../../core/Base"; 9 | import { Peer } from "../../core/Peer"; 10 | import type { World } from "../../core/World"; 11 | import type { TileData } from "@growserver/types"; 12 | import { ExtendBuffer, DialogBuilder } from "@growserver/utils"; 13 | import { Tile } from "../Tile"; 14 | 15 | export class SwitcheROO extends Tile { 16 | constructor( 17 | public base: Base, 18 | public world: World, 19 | public data: TileData, 20 | ) { 21 | super(base, world, data); 22 | } 23 | 24 | public async onPunch(peer: Peer): Promise { 25 | if ( 26 | await this.world.hasTilePermission( 27 | peer.data.userID, 28 | this.data, 29 | LockPermission.BREAK, 30 | ) 31 | ) { 32 | // default punch behaviour, but with an exception 33 | this.data.flags ^= TileFlags.OPEN; 34 | } else { 35 | if (this.data.flags & TileFlags.PUBLIC) { 36 | this.data.flags ^= TileFlags.OPEN; 37 | this.applyDamage(peer, 0); 38 | } 39 | } 40 | 41 | return super.onPunch(peer); 42 | } 43 | 44 | public async onWrench(peer: Peer): Promise { 45 | const itemMeta = this.base.items.metadata.items.get( 46 | this.data.fg.toString(), 47 | )!; 48 | if ( 49 | await this.world.hasTilePermission( 50 | peer.data.userID, 51 | this.data, 52 | LockPermission.BUILD && itemMeta.flags! & BlockFlags.WRENCHABLE, 53 | ) 54 | ) { 55 | const dialog = new DialogBuilder() 56 | .defaultColor() 57 | .addLabelWithIcon( 58 | `\`wEdit ${itemMeta.name}\`\``, 59 | itemMeta.id as number, 60 | "big", 61 | ) 62 | .addCheckbox( 63 | "checkbox_public", 64 | "Usable by public", 65 | this.data.flags & TileFlags.PUBLIC ? "selected" : "not_selected", 66 | ) 67 | .embed("tilex", this.data.x) 68 | .embed("tiley", this.data.y) // i dont think this is included in the official one, but not very sure on it too. 69 | .endDialog("switcheroo_edit", "Cancel", "OK") 70 | .str(); 71 | 72 | peer.send(Variant.from("OnDialogRequest", dialog)); 73 | return true; 74 | } 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/utils/util/Color.ts: -------------------------------------------------------------------------------- 1 | export class Color { 2 | private colors: Uint8Array = new Uint8Array(4); 3 | 4 | constructor(r: number, g: number, b: number, a: number = 255) { 5 | if ( 6 | r < 0 || 7 | r > 255 || 8 | g < 0 || 9 | g > 255 || 10 | b < 0 || 11 | b > 255 || 12 | a < 0 || 13 | a > 255 14 | ) { 15 | throw new Error( 16 | "Invalid color values. Each value must be between 0 and 255.", 17 | ); 18 | } 19 | 20 | this.colors[0] = b; 21 | this.colors[1] = g; 22 | this.colors[2] = r; 23 | this.colors[3] = a; 24 | } 25 | 26 | public toDecimal(): number { 27 | let result = 0; 28 | for (let index = 0; index < this.colors.length; index++) { 29 | result = (result << 8) + this.colors[index]; 30 | } 31 | return result >>> 0; 32 | } 33 | 34 | public setRed(col: number): void { 35 | this.colors[2] = col; 36 | } 37 | 38 | public red(): number { 39 | return this.colors[2]; 40 | } 41 | 42 | public setGreen(col: number): void { 43 | this.colors[1] = col; 44 | } 45 | 46 | public green(): number { 47 | return this.colors[1]; 48 | } 49 | 50 | public setBlue(col: number): void { 51 | this.colors[0] = col; 52 | } 53 | 54 | public blue(): number { 55 | return this.colors[0]; 56 | } 57 | 58 | public setAlpha(col: number): void { 59 | this.colors[3] = col; 60 | } 61 | 62 | public alpha(): number { 63 | return this.colors[3]; 64 | } 65 | 66 | public static fromHex(hex: string): Color { 67 | if (!/^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(hex)) { 68 | throw new Error("Invalid hex color string."); 69 | } 70 | 71 | hex = hex.replace(/^#/, ""); 72 | 73 | const r = parseInt(hex.substring(0, 2), 16); 74 | const g = parseInt(hex.substring(2, 4), 16); 75 | const b = parseInt(hex.substring(4, 6), 16); 76 | const a = hex.length === 8 ? parseInt(hex.substring(6, 8), 16) : 255; 77 | 78 | return new Color(r, g, b, a); 79 | } 80 | 81 | public static fromDecimal(decimal: number): Color { 82 | if (decimal < 0 || decimal > 0xffffffff) { 83 | throw new Error( 84 | "Invalid decimal color value. It must be between 0 and 4294967295.", 85 | ); 86 | } 87 | 88 | const a = (decimal >> 24) & 0xff; 89 | const r = (decimal >> 16) & 0xff; 90 | const g = (decimal >> 8) & 0xff; 91 | const b = decimal & 0xff; 92 | 93 | return new Color(r, g, b, a); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /apps/server/src/world/tiles/SignTile.ts: -------------------------------------------------------------------------------- 1 | import { Variant } from "growtopia.js"; 2 | import { 3 | BlockFlags, 4 | LockPermission, 5 | TileExtraTypes, 6 | TileFlags, 7 | } from "@growserver/const"; 8 | import type { Base } from "../../core/Base"; 9 | import type { World } from "../../core/World"; 10 | import type { TileData } from "@growserver/types"; 11 | import { ExtendBuffer, DialogBuilder } from "@growserver/utils"; 12 | import { Tile } from "../Tile"; 13 | import { Peer } from "../../core/Peer"; 14 | import { ItemDefinition } from "grow-items"; 15 | 16 | export class SignTile extends Tile { 17 | public extraType = TileExtraTypes.SIGN; 18 | 19 | constructor( 20 | public base: Base, 21 | public world: World, 22 | public data: TileData, 23 | ) { 24 | super(base, world, data); 25 | } 26 | 27 | public async onPlaceForeground( 28 | peer: Peer, 29 | itemMeta: ItemDefinition, 30 | ): Promise { 31 | if (!(await super.onPlaceForeground(peer, itemMeta))) { 32 | return false; 33 | } 34 | 35 | this.data.flags |= TileFlags.TILEEXTRA; 36 | this.data.sign = { label: "" }; 37 | 38 | return true; 39 | } 40 | 41 | public async onDestroy(peer: Peer): Promise { 42 | await super.onDestroy(peer); 43 | this.data.sign = undefined; 44 | } 45 | 46 | public async onWrench(peer: Peer): Promise { 47 | if (!(await super.onWrench(peer))) { 48 | this.onPlaceFail(peer); 49 | return false; 50 | } 51 | 52 | const itemMeta = this.base.items.metadata.items.get( 53 | this.data.fg.toString(), 54 | )!; 55 | const dialog = new DialogBuilder() 56 | .defaultColor() 57 | .addLabelWithIcon( 58 | `\`wEdit ${itemMeta.name}\`\``, 59 | itemMeta.id as number, 60 | "big", 61 | ) 62 | .addTextBox("What would you like to write on this sign?") 63 | .addInputBox("label", "", this.data.sign?.label, 100) 64 | .embed("tilex", this.data.x) 65 | .embed("tiley", this.data.y) 66 | .endDialog("sign_edit", "Cancel", "OK") 67 | .str(); 68 | 69 | peer.send(Variant.from("OnDialogRequest", dialog)); 70 | return true; 71 | } 72 | 73 | public async serialize(dataBuffer: ExtendBuffer): Promise { 74 | await super.serialize(dataBuffer); 75 | dataBuffer.grow(1 + 2 + (this.data.sign?.label?.length ?? 0) + 4); 76 | dataBuffer.writeU8(this.extraType); 77 | dataBuffer.writeString(this.data.sign?.label || ""); 78 | dataBuffer.writeI32(-1); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/DoorEdit.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { TileData } from "@growserver/types"; 5 | import { World } from "../../core/World"; 6 | import { Tile } from "../../world/Tile"; 7 | import { ItemDefinition } from "grow-items"; 8 | import { tileFrom } from "../../world/tiles"; 9 | import { LockPermission, TileFlags } from "@growserver/const"; 10 | 11 | export class DoorEdit { 12 | private world: World; 13 | private pos: number; 14 | private block: TileData; 15 | private itemMeta: ItemDefinition; 16 | 17 | constructor( 18 | public base: Base, 19 | public peer: Peer, 20 | public action: NonEmptyObject<{ 21 | dialog_name: string; 22 | tilex: string; 23 | tiley: string; 24 | itemID: string; 25 | label: string; 26 | target: string; 27 | checkbox_public: string; 28 | id: string; 29 | }>, 30 | ) { 31 | this.world = this.peer.currentWorld()!; 32 | this.pos = 33 | parseInt(this.action.tilex) + 34 | parseInt(this.action.tiley) * (this.world?.data.width as number); 35 | this.block = this.world?.data.blocks[this.pos] as TileData; 36 | this.itemMeta = this.base.items.metadata.items.find( 37 | (i) => i.id === parseInt(action.itemID), 38 | )!; 39 | } 40 | 41 | public async execute(): Promise { 42 | if ( 43 | !this.action.dialog_name || 44 | !this.action.tilex || 45 | !this.action.tiley || 46 | !this.action.itemID || 47 | !this.action.label || 48 | !this.action.target || 49 | !this.action.checkbox_public || 50 | !this.action.id 51 | ) 52 | return; 53 | if ( 54 | !(await this.world.hasTilePermission( 55 | this.peer.data.userID, 56 | this.block, 57 | LockPermission.BUILD, 58 | )) || 59 | !this.block.door 60 | ) { 61 | return; 62 | } 63 | 64 | this.block.door = { 65 | label: this.action.label || "", 66 | destination: this.action.target?.toUpperCase() || "", 67 | id: this.action.id?.toUpperCase() || "", 68 | }; 69 | 70 | if (this.action.checkbox_public == "1") { 71 | this.block.flags |= TileFlags.PUBLIC; 72 | } else { 73 | this.block.flags &= ~TileFlags.PUBLIC; // unset PUBLIC flag 74 | } 75 | 76 | const doorTile = tileFrom(this.base, this.world, this.block); 77 | this.world.every((p) => doorTile.tileUpdate(p)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /apps/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "3.1.5", 4 | "description": "A Growtopia private server built with Node.js and Bun.js, powered by growtopia.js", 5 | "main": "dist/src/app.js", 6 | "scripts": { 7 | "test": "nr build && (node dist/src/app.js || bun run dist/src/app.js)", 8 | "lint": "eslint ./src", 9 | "lint:fix": "eslint ./src --fix", 10 | "bun": "cross-env RUNTIME_ENV=bun bun run --bun", 11 | "node": "cross-env RUNTIME_ENV=node node", 12 | "tsx": "cross-env RUNTIME_ENV=node tsx", 13 | "dev": "cross-env RUNTIME_ENV=node tsx ./src/app.ts", 14 | "push": "drizzle-kit push", 15 | "seed": "nr tsx scripts/seeds.ts || nr bun scripts/seeds.ts", 16 | "studio": "drizzle-kit studio", 17 | "build": "rimraf dist && tsc", 18 | "setup": "nr push && nr tsx scripts/setup.ts || nr bun scripts/setup.ts", 19 | "clean": "rimraf .cache dist", 20 | "iteminfo": "nr tsx scripts/item-info/build.ts || nr bun scripts/item-info/build.ts", 21 | "install:server": "nr setup && nr build && nr push && nr seed && nr iteminfo", 22 | "start": "nr start:bun || nr start:node", 23 | "start:bun": "nr bun dist/src/app.js", 24 | "start:node": "nr node dist/src/app.js" 25 | }, 26 | "keywords": [], 27 | "author": "JadlionHD ", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@antfu/ni": "^25.0.0", 31 | "@growserver/config": "workspace:*", 32 | "@growserver/const": "workspace:*", 33 | "@growserver/db": "workspace:*", 34 | "@growserver/logger": "workspace:*", 35 | "@growserver/types": "workspace:*", 36 | "@growserver/utils": "workspace:*", 37 | "bcryptjs": "^3.0.2", 38 | "chokidar": "^4.0.3", 39 | "cross-env": "^10.0.0", 40 | "dotenv": "^17.2.2", 41 | "drizzle-orm": "^0.44.5", 42 | "fast-xml-parser": "^5.2.5", 43 | "grow-items": "^1.3.1", 44 | "growtopia.js": "^2.1.6", 45 | "jsonwebtoken": "^9.0.2", 46 | "ky": "^1.10.0", 47 | "mwparser": "^1.3.3", 48 | "nanoid": "5.1.5", 49 | "rimraf": "^6.1.0", 50 | "tsx": "^4.20.5" 51 | }, 52 | "devDependencies": { 53 | "@eslint/js": "^9.35.0", 54 | "@stylistic/eslint-plugin-ts": "^4.4.1", 55 | "@types/bun": "^1.2.21", 56 | "@types/jsonwebtoken": "^9.0.10", 57 | "@types/node": "^24.3.1", 58 | "drizzle-kit": "^0.31.4", 59 | "eslint": "^9.35.0", 60 | "jiti": "^2.5.1", 61 | "type-fest": "^4.41.0", 62 | "typescript": "^5.9.2", 63 | "typescript-eslint": "^8.42.0" 64 | }, 65 | "engines": { 66 | "node": ">=18.0.0", 67 | "npm": ">=7.0.0" 68 | } 69 | } -------------------------------------------------------------------------------- /apps/server/src/command/cmds/sdb.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant, TextPacket, PacketTypes } from "growtopia.js"; 6 | import { DialogBuilder } from "@growserver/utils"; 7 | 8 | export default class Sdb extends Command { 9 | constructor( 10 | public base: Base, 11 | public peer: Peer, 12 | public text: string, 13 | public args: string[], 14 | ) { 15 | super(base, peer, text, args); 16 | this.opt = { 17 | command: ["sdb"], 18 | description: "Send a global message to everyone via a dialog box", 19 | cooldown: 5, 20 | ratelimit: 1, 21 | category: "`bDev", 22 | usage: "/sdb ", 23 | example: ["/sdb Hello everyone!"], 24 | permission: [ROLE.DEVELOPER], 25 | }; 26 | } 27 | 28 | public async execute(): Promise { 29 | if (this.args.length === 0) 30 | return this.peer.send( 31 | Variant.from("OnConsoleMessage", "Message are required."), 32 | ); 33 | 34 | const message = this.args.join(" "); 35 | const senderName = this.peer.data.displayName; 36 | const world = this.peer.currentWorld(); 37 | const jammed = world?.data.jammers?.find( 38 | (v) => v.type === "signal", 39 | )?.enabled; 40 | 41 | // Dialog box creation 42 | const dialog = new DialogBuilder() 43 | .defaultColor() 44 | .addLabelWithIcon("Super Duper Broadcast", "2480", "big") 45 | .addSpacer("small") 46 | .addSmallText(`\`oMessage from: \`$${senderName}`); 47 | 48 | // If no jammer, show the world name 49 | if (!jammed) { 50 | dialog.addSmallText(`\`oWorld: \`o${this.peer.data.world}`); 51 | } else { 52 | dialog.addSmallText("`4JAMMED`"); 53 | } 54 | 55 | dialog 56 | .addSpacer("small") 57 | .addSmallText(`\`5${message}`) 58 | .addQuickExit() 59 | .endDialog("ok", "Close", ""); 60 | 61 | // Send dialog box and play beep sound to all players 62 | this.peer.every((player) => { 63 | player.send( 64 | Variant.from("OnDialogRequest", dialog.str()), 65 | TextPacket.from( 66 | PacketTypes.ACTION, 67 | "action|play_sfx", 68 | `file|audio/beep.wav`, 69 | `delayMS|0`, 70 | ), 71 | ); 72 | }); 73 | 74 | // Confirmation to sender 75 | this.peer.send( 76 | Variant.from( 77 | "OnConsoleMessage", 78 | `\`2Super Duper Broadcast sent to all players.`, 79 | ), 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/server/src/world/generation/Default.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TileFlags, 3 | Y_END_DIRT, 4 | Y_LAVA_START, 5 | Y_START_DIRT, 6 | } from "@growserver/const"; 7 | import { TileData, WorldData } from "@growserver/types"; 8 | import { WorldGen } from "../WorldGen"; 9 | 10 | export class Default extends WorldGen { 11 | public data: WorldData; 12 | public width: number; 13 | public height: number; 14 | public blockCount: number; 15 | 16 | constructor(public name: string) { 17 | super(name); 18 | this.width = 100; 19 | this.height = 60; 20 | this.blockCount = this.height * this.width; 21 | 22 | this.data = { 23 | name, 24 | width: this.width, 25 | height: this.height, 26 | blocks: [], 27 | playerCount: 0, 28 | jammers: [], // separate to different table 29 | dropped: { 30 | // separate (maybe?) to different table 31 | uid: 0, 32 | items: [], 33 | }, 34 | weather: { id: 41 }, 35 | }; 36 | } 37 | 38 | public generate(): Promise { 39 | return new Promise((res, _rej) => { 40 | // starting points 41 | let x = 0; 42 | let y = 0; 43 | // main door location 44 | const mainDoorPosition = Math.floor(Math.random() * this.width); 45 | 46 | for (let i = 0; i < this.blockCount; i++) { 47 | // increase y axis, reset back to 0 48 | if (x >= this.width) { 49 | y++; 50 | x = 0; 51 | } 52 | 53 | const block: TileData = { 54 | x, 55 | y, 56 | fg: 0, 57 | bg: 0, 58 | flags: 0, 59 | }; 60 | 61 | if (block.y === Y_START_DIRT - 1 && block.x === mainDoorPosition) { 62 | block.fg = 6; 63 | block.door = { 64 | label: "EXIT", 65 | destination: "EXIT", 66 | }; 67 | block.flags |= TileFlags.PUBLIC | TileFlags.TILEEXTRA; 68 | } else if (block.y >= Y_START_DIRT) { 69 | block.fg = 70 | block.x === mainDoorPosition && block.y === Y_START_DIRT 71 | ? 8 72 | : block.y < Y_END_DIRT 73 | ? block.y >= Y_LAVA_START 74 | ? Math.random() > 0.2 75 | ? Math.random() > 0.1 76 | ? 2 77 | : 10 78 | : 4 79 | : Math.random() > 0.01 80 | ? 2 81 | : 10 82 | : 8; 83 | block.bg = 14; 84 | } 85 | 86 | this.data.blocks.push(block); 87 | 88 | x++; 89 | } 90 | res(); 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/Find.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | import { DialogBuilder } from "@growserver/utils"; 7 | 8 | export default class Find extends Command { 9 | constructor( 10 | public base: Base, 11 | public peer: Peer, 12 | public text: string, 13 | public args: string[], 14 | ) { 15 | super(base, peer, text, args); 16 | this.opt = { 17 | command: ["find"], 18 | description: "Find some items", 19 | cooldown: 5, 20 | ratelimit: 5, 21 | category: "`oBasic", 22 | usage: "/find ", 23 | example: ["/find", "/find dirt"], 24 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 25 | }; 26 | } 27 | 28 | public async execute(): Promise { 29 | const dialog = new DialogBuilder() 30 | .defaultColor() 31 | .addLabelWithIcon("Find the item", "6016", "big") 32 | .addCheckbox("seed_only", "Only seed", "not_selected") 33 | .addInputBox("find_item_name", "", "", 30) 34 | .addQuickExit() 35 | .endDialog("find_item", "Cancel", "Find") 36 | .str(); 37 | 38 | if (this.args.length) { 39 | const findItemName = this.args.join(" "); 40 | const isSeed = false; 41 | const dialog = new DialogBuilder() 42 | .defaultColor() 43 | .addQuickExit() 44 | .addLabelWithIcon("Find the item", "6016", "big") 45 | .addSpacer("small"); 46 | 47 | const items = this.base.items.metadata.items.filter((v) => 48 | v.name?.toLowerCase().includes(findItemName.toLowerCase()), 49 | ); 50 | items.forEach((item) => { 51 | const itemID = item.id || 0; 52 | const itemName = item.name || ""; 53 | if (isSeed) { 54 | if (itemID % 2 === 1) 55 | dialog.addButtonWithIcon( 56 | itemID, 57 | itemID, 58 | itemName, 59 | "staticBlueFrame", 60 | item.id, 61 | ); 62 | } else { 63 | if (itemID % 2 === 0) 64 | dialog.addButtonWithIcon( 65 | itemID, 66 | itemID, 67 | itemName, 68 | "staticBlueFrame", 69 | item.id, 70 | ); 71 | } 72 | }); 73 | 74 | dialog.endDialog("find_item_end", "Cancel", ""); 75 | 76 | this.peer.send(Variant.from("OnDialogRequest", dialog.str())); 77 | return; 78 | } 79 | 80 | this.peer.send(Variant.from("OnDialogRequest", dialog)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/db/handlers/Player.ts: -------------------------------------------------------------------------------- 1 | import { type PostgresJsDatabase } from "drizzle-orm/postgres-js"; 2 | import { eq, sql, like } from "drizzle-orm"; 3 | import { players } from "../shared/schemas/Player"; 4 | import bcrypt from "bcryptjs"; 5 | import { ROLE } from "@growserver/const"; 6 | import { PeerData } from "@growserver/types"; 7 | 8 | export class PlayerDB { 9 | constructor(private db: PostgresJsDatabase>) {} 10 | 11 | public async get(name: string) { 12 | const res = await this.db 13 | .select() 14 | .from(players) 15 | .where(like(players.name, name)) 16 | .limit(1) 17 | .execute(); 18 | 19 | if (res.length) return res[0]; 20 | return undefined; 21 | } 22 | 23 | public async getByUID(userID: number) { 24 | const res = await this.db 25 | .select() 26 | .from(players) 27 | .where(eq(players.id, userID)) 28 | .limit(1) 29 | .execute(); 30 | 31 | if (res.length) return res[0]; 32 | return undefined; 33 | } 34 | 35 | public async has(name: string) { 36 | const res = await this.db 37 | .select({ count: sql`count(*)` }) 38 | .from(players) 39 | .where(like(players.name, name)) 40 | .limit(1) 41 | .execute(); 42 | 43 | return (res[0].count as number) > 0; 44 | } 45 | 46 | public async set(name: string, password: string) { 47 | const salt = await bcrypt.genSalt(10); 48 | const hashPassword = await bcrypt.hash(password, salt); 49 | 50 | const res = await this.db 51 | .insert(players) 52 | .values({ 53 | display_name: name, 54 | name: name.toLowerCase(), 55 | password: hashPassword, 56 | role: ROLE.BASIC, 57 | heart_monitors: JSON.stringify({}), 58 | }) 59 | .returning({ id: players.id }); 60 | 61 | if (res.length && res[0].id) return res[0].id; 62 | return 0; 63 | } 64 | 65 | public async save(data: PeerData) { 66 | if (!data.userID) return false; 67 | 68 | const res = await this.db 69 | .update(players) 70 | .set({ 71 | name: data.name, 72 | display_name: data.displayName, 73 | role: data.role, 74 | inventory: JSON.stringify(data.inventory), 75 | clothing: JSON.stringify(data.clothing), 76 | gems: data.gems, 77 | level: data.level, 78 | exp: data.exp, 79 | last_visited_worlds: JSON.stringify(data.lastVisitedWorlds), 80 | updated_at: new Date().toISOString().slice(0, 19).replace("T", " "), 81 | heart_monitors: JSON.stringify(Object.fromEntries(data.heartMonitors)), 82 | }) 83 | .where(eq(players.id, data.userID)) 84 | .returning({ id: players.id }); 85 | 86 | if (res.length) return true; 87 | else return false; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/db/scripts/seeds.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { players } from "../"; 4 | // import { worlds } from "../src/database/schemas/World"; 5 | import { drizzle } from "drizzle-orm/postgres-js"; 6 | import postgres from "postgres"; 7 | import bcrypt from "bcryptjs"; 8 | import { config } from "dotenv"; 9 | 10 | config({ 11 | path: "../../.env", 12 | }); 13 | 14 | /** 15 | * @param {string} password 16 | */ 17 | async function hash(password: string) { 18 | const salt = await bcrypt.genSalt(10); 19 | 20 | return await bcrypt.hash(password, salt); 21 | } 22 | 23 | export async function setupSeeds() { 24 | const connection = postgres(process.env.DATABASE_URL!); 25 | const db = drizzle(connection); 26 | const dateNow = new Date().toISOString().slice(0, 19).replace("T", " "); 27 | 28 | // Drop and recreate table if it exists 29 | await connection.unsafe(`DROP TABLE IF EXISTS players CASCADE;`); 30 | await connection.unsafe(` 31 | CREATE TABLE IF NOT EXISTS players ( 32 | id SERIAL PRIMARY KEY, 33 | name TEXT NOT NULL UNIQUE, 34 | display_name TEXT NOT NULL, 35 | password TEXT NOT NULL, 36 | role TEXT NOT NULL, 37 | gems INTEGER DEFAULT 0, 38 | level INTEGER DEFAULT 0, 39 | exp INTEGER DEFAULT 0, 40 | clothing TEXT, 41 | inventory TEXT, 42 | last_visited_worlds TEXT, 43 | created_at TEXT DEFAULT (current_timestamp), 44 | updated_at TEXT DEFAULT (current_timestamp), 45 | heart_monitors TEXT NOT NULL 46 | ); 47 | `); 48 | await db 49 | .insert(players) 50 | .values([ 51 | { 52 | name: "admin", 53 | display_name: "admin", 54 | password: await hash("admin"), 55 | role: "1", 56 | gems: 1000, 57 | clothing: null, 58 | inventory: null, 59 | last_visited_worlds: null, 60 | created_at: dateNow, 61 | heart_monitors: JSON.stringify({}), // intialize empty object. 62 | }, 63 | { 64 | name: "reimu", 65 | display_name: "Reimu", 66 | password: await hash("hakurei"), 67 | role: "2", 68 | gems: 1000, 69 | clothing: null, 70 | inventory: null, 71 | last_visited_worlds: null, 72 | created_at: dateNow, 73 | heart_monitors: JSON.stringify({}), // intialize empty object. 74 | }, 75 | { 76 | name: "jadlionhd", 77 | display_name: "JadlionHD", 78 | password: await hash("admin"), 79 | role: "1", 80 | gems: 1000, 81 | clothing: null, 82 | inventory: null, 83 | last_visited_worlds: null, 84 | created_at: dateNow, 85 | heart_monitors: JSON.stringify({}), // intialize empty object. 86 | }, 87 | ]) 88 | .onConflictDoNothing(); // dont confuse the normal user with error lol 89 | } 90 | -------------------------------------------------------------------------------- /apps/server/src/network/dialogs/TrashEnd.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { Variant, TextPacket, PacketTypes } from "growtopia.js"; 5 | 6 | export class TrashEnd { 7 | constructor( 8 | public base: Base, 9 | public peer: Peer, 10 | public action: NonEmptyObject<{ 11 | dialog_name: string; 12 | trash_count: string; 13 | itemID: string; 14 | }>, 15 | ) {} 16 | 17 | public async execute(): Promise { 18 | if ( 19 | !this.action.dialog_name || 20 | !this.action.trash_count || 21 | !this.action.itemID 22 | ) 23 | return; 24 | if (!/\d/.test(this.action.trash_count) || !/\d/.test(this.action.itemID)) { 25 | this.peer.send( 26 | Variant.from( 27 | "OnTalkBubble", 28 | this.peer.data.netID, 29 | "Uh oh, thats not a valid number", 30 | ), 31 | ); 32 | return; 33 | } 34 | const itemID = parseInt(this.action.itemID); 35 | const count = parseInt(this.action.trash_count); 36 | const itemExist = this.peer.data?.inventory?.items.find( 37 | (i) => i.id === itemID, 38 | ); 39 | if (!itemExist || itemExist.amount <= 0) { 40 | this.peer.send( 41 | Variant.from( 42 | "OnTalkBubble", 43 | this.peer.data.netID, 44 | "That item, seems not exist in your inventory", 45 | ), 46 | ); 47 | return; 48 | } 49 | 50 | if (count > itemExist.amount) { 51 | this.peer.send( 52 | Variant.from("OnTalkBubble", this.peer.data.netID, "Really?"), 53 | ); 54 | return; 55 | } 56 | 57 | if (itemID === 18 || itemID === 32) { 58 | this.peer.send( 59 | Variant.from( 60 | "OnTalkBubble", 61 | this.peer.data.netID, 62 | "Cannot trash this item.", 63 | ), 64 | ); 65 | return; 66 | } 67 | 68 | if (count <= 0) { 69 | this.peer.send( 70 | Variant.from( 71 | "OnTalkBubble", 72 | this.peer.data.netID, 73 | "Nice try. You remind me of myself at that age.", 74 | ), 75 | ); 76 | return; 77 | } 78 | // Use the RemoveItemInven Method Instead of Direct Subtraction 79 | this.peer.removeItemInven(itemID, count); 80 | this.peer.send( 81 | TextPacket.from( 82 | PacketTypes.ACTION, 83 | "action|play_sfx", 84 | "file|audio/trash.wav", 85 | "delayMS|0", 86 | ), 87 | ); 88 | const item = this.base.items.metadata.items.find((v) => v.id === itemID); 89 | //this.peer.inventory(); 90 | this.peer.sendClothes(); 91 | this.peer.send( 92 | Variant.from("OnConsoleMessage", `You trashed \`w${count} ${item?.name}`), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /apps/server/src/world/tiles/WeatherTile.ts: -------------------------------------------------------------------------------- 1 | import type { Base } from "../../core/Base"; 2 | import { Peer } from "../../core/Peer"; 3 | import type { World } from "../../core/World"; 4 | import type { TileData } from "@growserver/types"; 5 | import { ExtendBuffer } from "@growserver/utils"; 6 | import { Tile } from "../Tile"; 7 | import { ActionTypes, LockPermission, TileExtraTypes } from "@growserver/const"; 8 | import { ItemDefinition } from "grow-items"; 9 | import { Variant } from "growtopia.js"; 10 | 11 | export class WeatherTile extends Tile { 12 | constructor( 13 | public base: Base, 14 | public world: World, 15 | public data: TileData, 16 | ) { 17 | super(base, world, data); 18 | } 19 | 20 | public async onPunch(peer: Peer): Promise { 21 | // Check if player has break permission 22 | if ( 23 | !(await this.world.hasTilePermission( 24 | peer.data.userID, 25 | this.data, 26 | LockPermission.BREAK, 27 | )) 28 | ) { 29 | return super.onPunch(peer); 30 | } 31 | 32 | const itemMeta = this.base.items.metadata.items.get( 33 | this.data.fg.toString(), 34 | ); 35 | 36 | // if the block still has any damage, do not toggle. 37 | if (this.data.resetStateAt && this.data.resetStateAt > Date.now()) 38 | return super.onPunch(peer); 39 | 40 | // they used this as the weather id. Lol 41 | const targetWeatherId = itemMeta?.audioVolume; 42 | if (!targetWeatherId) { 43 | peer.sendConsoleMessage( 44 | `[Weather] Unknown mapping for item ${this.data.fg}. Keeping weather: ${this.world.data.weather.id}`, 45 | ); 46 | return await super.onPunch(peer); 47 | } 48 | 49 | // Toggle: if already active, set to default 41 (clear); else set to mapped id 50 | const newWeatherId = 51 | this.world.data.weather.id === targetWeatherId ? 41 : targetWeatherId; 52 | this.world.data.weather.id = newWeatherId; 53 | 54 | // Broadcast change to all peers in the world 55 | this.world.every((p) => { 56 | p.send(Variant.from("OnSetCurrentWeather", this.world.data.weather.id)); 57 | }); 58 | 59 | // Also apply normal damage/break flow so the machine can be broken 60 | return await super.onPunch(peer); 61 | } 62 | 63 | public async serialize(dataBuffer: ExtendBuffer): Promise { 64 | await super.serialize(dataBuffer); 65 | // there were no exta data here lol. Srry 66 | } 67 | 68 | // Clear weather when weather machine is broken 69 | public async onDestroy(peer: Peer): Promise { 70 | await super.onDestroy(peer); 71 | // this is the old default backgound with no ads and things. 72 | if (this.world.data.weather.id !== 41) { 73 | this.world.data.weather.id = 41; 74 | this.world.every((p) => { 75 | p.send(Variant.from("OnSetCurrentWeather", 41)); 76 | }); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/db/handlers/World.ts: -------------------------------------------------------------------------------- 1 | import { type PostgresJsDatabase } from "drizzle-orm/postgres-js"; 2 | import { eq, sql } from "drizzle-orm"; 3 | import { worlds } from "../shared/schemas/World"; 4 | import { WorldData } from "@growserver/types"; 5 | 6 | export class WorldDB { 7 | constructor(private db: PostgresJsDatabase>) {} 8 | 9 | public async get(name: string) { 10 | const res = await this.db 11 | .select() 12 | .from(worlds) 13 | .where(eq(worlds.name, name)) 14 | .limit(1) 15 | .execute(); 16 | 17 | if (res.length) return res[0]; 18 | return undefined; 19 | } 20 | 21 | public async has(name: string) { 22 | const res = await this.db 23 | .select({ count: sql`count(*)` }) 24 | .from(worlds) 25 | .where(eq(worlds.name, name)) 26 | .limit(1) 27 | .execute(); 28 | 29 | return (res[0].count as number) > 0; 30 | } 31 | 32 | public async set(data: WorldData) { 33 | if (!data.name && !data.blocks && !data.width && !data.height) return 0; 34 | 35 | const worldLockData = data.worldLockIndex 36 | ? data.blocks[data.worldLockIndex].lock 37 | : null; 38 | 39 | const res = await this.db 40 | .insert(worlds) 41 | .values({ 42 | name: data.name, 43 | ownedBy: worldLockData?.ownerUserID ?? null, 44 | width: data.width, 45 | height: data.height, 46 | blocks: JSON.stringify(data.blocks), 47 | // owner: data.owner ? Buffer.from(JSON.stringify(data.owner)) : null, 48 | dropped: JSON.stringify(data.dropped), 49 | updated_at: new Date().toISOString().slice(0, 19).replace("T", " "), 50 | weather_id: data.weather.id, 51 | worldlock_index: data.worldLockIndex, 52 | // minimum_level: data.minLevel 53 | }) 54 | .returning({ id: worlds.id }); 55 | 56 | if (res.length && res[0].id) return res[0].id; 57 | return 0; 58 | } 59 | 60 | public async save(data: WorldData) { 61 | if (!data.name && !data.blocks && !data.width && !data.height) return false; 62 | 63 | const worldLockData = data.worldLockIndex 64 | ? data.blocks[data.worldLockIndex].lock 65 | : null; 66 | 67 | const res = await this.db 68 | .update(worlds) 69 | .set({ 70 | ownedBy: worldLockData?.ownerUserID ?? null, 71 | width: data.width, 72 | height: data.height, 73 | blocks: JSON.stringify(data.blocks), // only save tile data here. 74 | // owner: data.owner ? Buffer.from(JSON.stringify(data.owner)) : null, 75 | dropped: JSON.stringify(data.dropped), 76 | updated_at: new Date().toISOString().slice(0, 19).replace("T", " "), 77 | weather_id: data.weather.id, 78 | // minimum_level: data.minLevel 79 | }) 80 | .where(eq(worlds.name, data.name)) 81 | .returning({ id: worlds.id }); 82 | 83 | if (res.length) return true; 84 | return false; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /apps/server/scripts/item-info/parser.ts: -------------------------------------------------------------------------------- 1 | import { XMLParser } from "fast-xml-parser"; 2 | import { TemplateParser } from "./template"; 3 | import type { ItemsInfo, ItemsPage } from "@growserver/types"; 4 | import { ItemDefinition } from "grow-items"; 5 | import { parse } from "mwparser"; 6 | 7 | export class Parser { 8 | public XParser: XMLParser; 9 | public TParser: TemplateParser; 10 | 11 | constructor( 12 | public itemPages: ItemsPage[], 13 | private readonly items: ItemDefinition[], 14 | ) { 15 | this.XParser = new XMLParser(); 16 | this.TParser = new TemplateParser(this.items); 17 | } 18 | 19 | public async pagesToItems() { 20 | const parsedItems: ItemsInfo[] = []; 21 | 22 | for (const page of this.itemPages) { 23 | parsedItems.push(...(await this.parseXMLPage(page))); 24 | } 25 | 26 | return parsedItems; 27 | } 28 | 29 | public async parseXMLPage(page: ItemsPage) { 30 | const parsedItems: ItemsInfo[] = []; 31 | let doc; 32 | 33 | try { 34 | doc = this.XParser.parse(page.text as string); 35 | } catch (e) { 36 | // console.error(e); 37 | } 38 | 39 | for (const item of page.items) { 40 | const paged = ( 41 | doc?.mediawiki?.page as { title: string; revision: { text: string } }[] 42 | )?.find((i) => i.title === item.name); 43 | const pageText = paged?.revision?.text; 44 | 45 | parsedItems.push(await this.parseItemData(item, pageText)); 46 | } 47 | return parsedItems; 48 | } 49 | 50 | public async parseItemData(item: ItemDefinition, pageText?: string) { 51 | const itemData: ItemsInfo = { 52 | id: item.id!, 53 | name: item.name!, 54 | recipe: { 55 | splice: [], 56 | }, 57 | func: { 58 | add: "", 59 | rem: "", 60 | }, 61 | chi: "", 62 | desc: "", 63 | }; 64 | 65 | if (!pageText) return itemData; 66 | else { 67 | const parsed_wiki = parse(pageText); // parses wikitext to a nodelist where you can easily filter templates 68 | 69 | for (const template of parsed_wiki.templates) { 70 | const name = template.name.toLowerCase(); 71 | 72 | switch (name) { 73 | case "item/mod": 74 | itemData.playMods = this.TParser.playMods(template); 75 | break; 76 | case "recipesplice": 77 | itemData.recipe!.splice = this.TParser.splice(template); 78 | break; 79 | case "item": 80 | [itemData.desc, itemData.chi as string] = 81 | this.TParser.item(template); 82 | break; 83 | case "added": 84 | itemData.func!.add = this.TParser.func(template); 85 | break; 86 | case "removed": 87 | itemData.func!.rem = this.TParser.func(template); 88 | break; 89 | default: 90 | // console.log(template.name); 91 | } 92 | } 93 | return itemData; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/EnterGame.ts: -------------------------------------------------------------------------------- 1 | import { Variant } from "growtopia.js"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { DialogBuilder } from "@growserver/utils"; 5 | import { type NonEmptyObject } from "type-fest"; 6 | import { World } from "../../core/World"; 7 | import { tileFrom, tileUpdateMultiple } from "../../world/tiles"; 8 | import { TileFlags } from "@growserver/const"; 9 | import { HeartMonitorTile } from "../../world/tiles/HeartMonitorTile"; 10 | 11 | export class EnterGame { 12 | constructor( 13 | public base: Base, 14 | public peer: Peer, 15 | ) {} 16 | 17 | public async execute( 18 | _action: NonEmptyObject>, 19 | ): Promise { 20 | const tes = new DialogBuilder() 21 | .defaultColor() 22 | .addLabelWithIcon("`wThe GrowServer Gazette``", "5016", "big") 23 | .addSpacer("small") 24 | .raw( 25 | "add_image_button||interface/banner-transparent.rttex|bannerlayout|||\n", 26 | ) 27 | .addTextBox("Welcome to GrowServer") 28 | .addQuickExit() 29 | .endDialog("gazzette_end", "Cancel", "Ok") 30 | .str(); 31 | this.peer.send( 32 | Variant.from( 33 | "OnRequestWorldSelectMenu", 34 | ` 35 | add_heading|Top Worlds| 36 | add_floater|START|0|0.5|3529161471 37 | add_floater|START1|0|0.5|3529161471 38 | add_floater|START2|0|0.5|3529161471 39 | ${Array.from(this.base.cache.worlds.values()) 40 | .sort((a, b) => (b.playerCount || 0) - (a.playerCount || 0)) 41 | .slice(0, 6) 42 | .map((v) => { 43 | if (v.playerCount) 44 | return `add_floater|${v.name}|${v.playerCount ?? 0}|0.5|3529161471\n`; 45 | else return ""; 46 | }) 47 | .join("\n")} 48 | add_heading|Recently Visited Worlds| 49 | ${this.peer.data.lastVisitedWorlds 50 | ?.reverse() 51 | .map((v) => { 52 | const count = this.base.cache.worlds.get(v)?.playerCount || 0; 53 | return `add_floater|${v}|${count ?? 0}|0.5|3417414143\n`; 54 | }) 55 | .join("\n")} 56 | `, 57 | ), 58 | Variant.from( 59 | "OnConsoleMessage", 60 | `Welcome ${this.peer.data.displayName}\`\` There are \`w${this.base.getPlayersOnline()}\`\` players online.`, 61 | ), 62 | Variant.from({ delay: 100 }, "OnDialogRequest", tes), 63 | ); 64 | 65 | this.peer.data.heartMonitors.forEach((indexes, worldName) => { 66 | const tiles = new Array(); 67 | const worldData = this.base.cache.worlds.get(worldName); 68 | 69 | if (!worldData || worldData.playerCount == 0) return; 70 | 71 | const world = new World(this.base, worldName); 72 | 73 | for (const index of indexes) { 74 | const heartMonitorTile = tileFrom( 75 | this.base, 76 | world, 77 | worldData.blocks[index], 78 | ); 79 | 80 | tiles.push(heartMonitorTile as HeartMonitorTile); 81 | } 82 | 83 | tileUpdateMultiple(world, tiles); 84 | }); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /apps/server/src/network/actions/Info.ts: -------------------------------------------------------------------------------- 1 | import { type NonEmptyObject } from "type-fest"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { DialogBuilder } from "@growserver/utils"; 5 | import { Variant } from "growtopia.js"; 6 | import { ItemsInfo } from "@growserver/types"; 7 | 8 | export class Info { 9 | private readonly items = new Map(); 10 | 11 | constructor( 12 | public base: Base, 13 | public peer: Peer, 14 | ) { 15 | // Use the already loaded items from Base 16 | if (base.items.wiki && Array.isArray(base.items.wiki)) { 17 | base.items.wiki.forEach((it) => { 18 | this.items.set(it.id, it); 19 | }); 20 | } else { 21 | console.error("Items wiki data not loaded properly in Base class"); 22 | } 23 | } 24 | 25 | public async execute( 26 | action: NonEmptyObject>, 27 | ): Promise { 28 | const id = parseInt(action.itemID, 10); 29 | if (isNaN(id)) return this.sendMessage("Invalid item ID."); 30 | const item = this.items.get(id); 31 | if (!item) return this.sendMessage("Item not found."); 32 | 33 | const dlg = new DialogBuilder() 34 | .defaultColor() 35 | .addLabelWithIcon(`\`wAbout ${item.name} (${item.id})`, item.id, "small") 36 | .addSpacer("small") 37 | .addSmallText(item.desc || "`oNo description available.") 38 | .addSpacer("small") 39 | .addSmallText("Rarity: `wTODO"); 40 | 41 | if (item.recipe?.splice?.length) { 42 | const seeds = item.recipe.splice 43 | .map((sid) => this.items.get(sid)?.name || sid) 44 | .join(" + "); 45 | dlg.addSmallText(`Recipe: ${seeds} = ${item.name}`).addSpacer("small"); 46 | } 47 | 48 | // Check if item has combine property via metadata instead 49 | const itemMeta = this.base.items.metadata.items.get(item.id.toString()); 50 | const hasTransmutation = itemMeta && itemMeta.actionType === 34; // ActionType 34 is commonly used for transmutable items 51 | 52 | if (hasTransmutation) { 53 | dlg.addSmallText("`oThis item can be transmuted."); 54 | } else if (!item.recipe?.splice?.length) { 55 | dlg.addSmallText("`oThis item cannot be spliced."); 56 | } 57 | 58 | // Check if item is permanent via metadata instead 59 | const isPermaItem = 60 | itemMeta && 61 | itemMeta.flags !== undefined && 62 | (itemMeta.flags & 0x100) === 0x100; // Check for ITEM_UNTRADEABLE flag which often indicates perma items 63 | 64 | if (isPermaItem) { 65 | dlg 66 | .addSpacer("small") 67 | .addSmallText( 68 | "`3This item can't be destroyed - smashing it will return it to your backpack if you have room!", 69 | ); 70 | } 71 | 72 | const out = dlg.addButton("info_end", "OK").str(); 73 | this.peer.send(Variant.from("OnDialogRequest", out)); 74 | } 75 | private sendMessage(msg: string) { 76 | this.peer.send(Variant.from("OnTextOverlay", msg)); 77 | } 78 | } 79 | 80 | export { Info as InfoCommand }; 81 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/Ghost.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | 7 | export default class Ghost extends Command { 8 | constructor( 9 | public base: Base, 10 | public peer: Peer, 11 | public text: string, 12 | public args: string[], 13 | ) { 14 | super(base, peer, text, args); 15 | this.opt = { 16 | command: ["ghost"], 17 | description: "Toggle ghost mode to to walk through blocks.", 18 | cooldown: 5, 19 | ratelimit: 1, 20 | category: "`oBasic", 21 | usage: "/ghost", 22 | example: ["/ghost"], 23 | permission: [ROLE.DEVELOPER], 24 | }; 25 | } 26 | 27 | public async execute(): Promise { 28 | this.peer.data.state.isGhost = !this.peer.data.state.isGhost; 29 | this.peer.data.state.canWalkInBlocks = this.peer.data.state.isGhost; 30 | 31 | if (this.peer.data.state.isGhost) { 32 | // TODO: Allow unlimited jump and downwards movement 33 | 34 | // Enable ghost mode: walk through blocks, double jump, and flying ability 35 | this.peer.data.state.mod |= 1 | (1 << 1) | (1 << 23); // WALK_IN_BLOCKS | DOUBLE_JUMP | HAVE_FLYING_PINEAPPLE 36 | this.peer.send( 37 | Variant.from( 38 | "OnConsoleMessage", 39 | "`2Ghost mode enabled``. You can now walk through blocks!", 40 | ), 41 | ); 42 | 43 | // Send semi-transparent white appearance (50% opacity) 44 | const world = this.peer.currentWorld(); 45 | if (world) { 46 | world.every((p) => { 47 | p.send( 48 | Variant.from( 49 | { netID: this.peer.data.netID }, 50 | "OnSetClothing", 51 | [ 52 | this.peer.data.clothing.hair, 53 | this.peer.data.clothing.shirt, 54 | this.peer.data.clothing.pants, 55 | ], 56 | [ 57 | this.peer.data.clothing.feet, 58 | this.peer.data.clothing.face, 59 | this.peer.data.clothing.hand, 60 | ], 61 | [ 62 | this.peer.data.clothing.back, 63 | this.peer.data.clothing.mask, 64 | this.peer.data.clothing.necklace, 65 | ], 66 | 0xffffff80, // White with 50% transparency 67 | [this.peer.data.clothing.ances, 0.0, 0.0], 68 | ), 69 | ); 70 | }); 71 | } 72 | } else { 73 | // Disable ghost mode 74 | this.peer.data.state.mod &= ~(1 | (1 << 1) | (1 << 23)); // Remove WALK_IN_BLOCKS | DOUBLE_JUMP | HAVE_FLYING_PINEAPPLE 75 | this.peer.send( 76 | Variant.from( 77 | "OnConsoleMessage", 78 | "`4Ghost mode disabled``. You are back to normal.", 79 | ), 80 | ); 81 | 82 | // Restore normal appearance 83 | this.peer.sendClothes(); 84 | } 85 | 86 | this.peer.sendState(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Example](/apps/server/assets/ignore/banner.png) 2 | 3 | > A Growtopia private server built with Node.js and Bun.js, powered by [growtopia.js](https://github.com/JadlionHD/growtopia.js) 4 | 5 | > [!NOTE] 6 | > This source is not production ready yet. In the future it will be using a [Docker](#docker) to deploy the server, feel free to join [Discord Server](https://discord.gg/sGrxfKZY5t) to discuss regarding this. 7 | 8 | ## Requirements 9 | 10 | - [Node.js](https://nodejs.org) v20+ or [Bun.js](https://bun.sh) v1.2.9+ 11 | - [pnpm](https://pnpm.io) v10 12 | - [mkcert](https://github.com/FiloSottile/mkcert) 13 | - [docker](https://docker.com/) 14 | - [docker-compose](https://docs.docker.com/compose/) (required) 15 | 16 | ## Setup 17 | 18 | To setup the server, first install necessary packages & settings by 19 | 20 | ``` 21 | $ pnpm install 22 | $ pnpm run setup 23 | ``` 24 | 25 | And congrats setup are done, simple as that! 26 | Now you just need to run the server by 27 | 28 | > [!NOTE] 29 | > It must be running PostgreSQL & Redis in background by using docker, please navigate to [docker](#docker) guide 30 | 31 | ``` 32 | $ pnpm run dev 33 | ``` 34 | 35 | ## Database 36 | 37 | Database that we moved to PostgreSQL from previous database SQLite. 38 | And for the ORM we are using [Drizzle-ORM](https://orm.drizzle.team/) 39 | 40 | To view the database you can run this command below: 41 | 42 | ``` 43 | $ pnpm run studio 44 | ``` 45 | 46 | and access it on here https://local.drizzle.studio/ 47 | 48 | ## Starting server 49 | 50 | To run the development server by 51 | 52 | ``` 53 | $ pnpm run start 54 | ``` 55 | 56 | ## Development 57 | 58 | In order to make new login system work you need to install [mkcert](https://github.com/FiloSottile/mkcert) on this [download page](https://github.com/FiloSottile/mkcert/releases) (I'd recommend using [Lets encrypt](https://letsencrypt.org/getting-started/) for production only) 59 | 60 | ### Local CA installation 61 | 62 | Install the mkcert local CA by 63 | 64 | ``` 65 | $ mkcert -install 66 | ``` 67 | 68 | ### Hosts 69 | 70 | For the hosts file you can see this example below 71 | 72 | ``` 73 | 127.0.0.1 www.growtopia1.com 74 | 127.0.0.1 www.growtopia2.com 75 | 127.0.0.1 login.growserver.app # New login system for development purposes 76 | ``` 77 | 78 | ## Docker 79 | 80 | To run the dockerized & running it automatically just run 81 | 82 | ```sh 83 | docker compose up -d 84 | ``` 85 | 86 | or you want to run the database & redis only (this were for development only) then simply running 87 | 88 | ```sh 89 | docker compose up -d db redis 90 | ``` 91 | 92 | ## Contributing 93 | 94 | Any contributions are welcome. 95 | 96 | There's few rules of contributing: 97 | 98 | - Code must match the existing code style. Please make sure to run `pnpm run lint` before submiting a PR. 99 | - The commit must take review first before merging into `main` branch. 100 | 101 | ## Links 102 | 103 | - [Discord Server](https://discord.gg/sGrxfKZY5t) 104 | 105 | ## Contributors 106 | 107 | Give a thumbs to these cool contributors: 108 | 109 | 110 | 111 | { 44 | dialog.addStoreButton( 45 | item.name, 46 | item.title, 47 | item.description, 48 | item.image || "", 49 | item.imagePos || { x: 0, y: 0 }, 50 | item.cost || "", 51 | ); 52 | }); 53 | return dialog; 54 | } 55 | 56 | public async execute( 57 | _action: NonEmptyObject>, 58 | ): Promise { 59 | const dialog = new DialogBuilder() 60 | .defaultColor() 61 | .raw("enable_tabs|1") 62 | .addSpacer("small") 63 | // Tabs 64 | .raw( 65 | "add_tab_button|main_menu|main|interface/large/btn_shop2.rttex||1|0|0|0||||-1|-1|||0|0|", 66 | ) 67 | .addSpacer("small") 68 | .raw( 69 | "add_tab_button|player_menu|player|interface/large/btn_shop2.rttex||0|1|0|0||||-1|-1|||0|0|", 70 | ) 71 | .addSpacer("small") 72 | .raw( 73 | "add_tab_button|locks_menu|packs|interface/large/btn_shop2.rttex||0|3|0|0||||-1|-1|||0|0|", 74 | ) 75 | .addSpacer("small") 76 | .raw( 77 | "add_tab_button|itempacks_menu|bigitems|interface/large/btn_shop2.rttex||0|4|0|0||||-1|-1|||0|0|", 78 | ) 79 | .addSpacer("small") 80 | .raw( 81 | "add_tab_button|creativity_menu|weather|interface/large/btn_shop2.rttex||0|5|0|0||||-1|-1|||0|0|", 82 | ) 83 | .addSpacer("small") 84 | .raw( 85 | "add_tab_button|token_menu|growtoken|interface/large/btn_shop2.rttex||0|2|0|0||||-1|-1|||0|0|", 86 | ) 87 | .addSpacer("small") 88 | .raw("add_banner|interface/large/gui_shop_featured_header.rttex|0|1|") 89 | .addSpacer("small"); 90 | // Items 91 | this.addMainItems(dialog); 92 | 93 | const finalDialog = dialog 94 | .endDialog("store_end", "Cancel", "OK") 95 | .addQuickExit() 96 | .str(); 97 | 98 | this.peer.send( 99 | Variant.from("OnSetVouchers", 0), 100 | Variant.from("OnStoreRequest", finalDialog), 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /apps/server/src/world/tiles/HeartMonitorTile.ts: -------------------------------------------------------------------------------- 1 | import { TileExtraTypes, TileFlags } from "@growserver/const"; 2 | import type { Base } from "../../core/Base"; 3 | import type { World } from "../../core/World"; 4 | import { type TileData } from "@growserver/types"; 5 | import { ExtendBuffer } from "@growserver/utils"; 6 | import { Tile } from "../Tile"; 7 | import { Peer } from "../../core/Peer"; 8 | import { ItemDefinition } from "grow-items"; 9 | 10 | export class HeartMonitorTile extends Tile { 11 | public extraType = TileExtraTypes.HEART_MONITOR; 12 | 13 | constructor( 14 | public base: Base, 15 | public world: World, 16 | public data: TileData, 17 | ) { 18 | super(base, world, data); 19 | } 20 | 21 | public async onPlaceForeground( 22 | peer: Peer, 23 | itemMeta: ItemDefinition, 24 | ): Promise { 25 | if (!(await super.onPlaceForeground(peer, itemMeta))) { 26 | return false; 27 | } 28 | 29 | this.data.heartMonitor = { userID: peer.data.userID }; 30 | this.data.flags |= TileFlags.TILEEXTRA; 31 | 32 | let heartMonitorArray = peer.data.heartMonitors.get(this.world.worldName); 33 | 34 | if (!heartMonitorArray) { 35 | heartMonitorArray = new Array(); 36 | } 37 | 38 | heartMonitorArray.push(this.data.y * this.world.data.width + this.data.x); 39 | peer.data.heartMonitors.set(this.world.worldName, heartMonitorArray); 40 | 41 | // await peer.saveToCache(); 42 | this.world.every((p) => this.tileUpdate(p)); 43 | return true; 44 | } 45 | 46 | public async onDestroy(peer: Peer): Promise { 47 | await super.onDestroy(peer); 48 | 49 | const idx = peer.data.heartMonitors 50 | .get(this.world.worldName)! 51 | .findIndex((v) => v == this.data.y * this.world.data.width + this.data.x); 52 | 53 | if (idx) peer.data.heartMonitors.get(this.world.worldName)!.splice(idx, 1); 54 | 55 | // await peer.saveToCache(); 56 | 57 | this.data.heartMonitor = undefined; 58 | } 59 | 60 | public async serialize(dataBuffer: ExtendBuffer): Promise { 61 | await super.serialize(dataBuffer); 62 | 63 | const user = await this.base.database.players.getByUID( 64 | this.data.heartMonitor!.userID, 65 | ); 66 | dataBuffer.grow(7 + (user?.display_name.length ?? 0)); 67 | dataBuffer.writeU8(this.extraType); 68 | dataBuffer.writeU32(this.data.heartMonitor!.userID); 69 | dataBuffer.writeString(user?.display_name ?? ""); 70 | } 71 | 72 | public async setFlags(flags: number): Promise { 73 | flags = await super.setFlags(flags); 74 | 75 | const targetPeer = this.base.cache.peers.find( 76 | (p) => p.userID == this.data.heartMonitor!.userID, 77 | ); 78 | 79 | if (targetPeer) { 80 | flags |= TileFlags.OPEN; 81 | } 82 | 83 | return flags; 84 | } 85 | 86 | // public async setFlags(): Promise { 87 | // this.flags |= TileFlags.TILEEXTRA; 88 | 89 | // if (this.block.rotatedLeft) this.flags |= TileFlags.ROTATED_LEFT; 90 | 91 | // const targetPeerId = this.base.cache.peers.find( 92 | // (v) => v.userID === this.userId 93 | // ); 94 | // if (targetPeerId) { 95 | // const targetPeer = new Peer(this.base, targetPeerId.netID); 96 | 97 | // if (targetPeer) this.flags |= TileFlags.OPEN; 98 | // } 99 | 100 | // return; 101 | // } 102 | } 103 | -------------------------------------------------------------------------------- /apps/server/src/command/cmds/Help.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "../Command"; 2 | import { Base } from "../../core/Base"; 3 | import { Peer } from "../../core/Peer"; 4 | import { ROLE } from "@growserver/const"; 5 | import { Variant } from "growtopia.js"; 6 | import { CommandMap } from "."; 7 | import { DialogBuilder } from "@growserver/utils"; 8 | 9 | export default class Help extends Command { 10 | constructor(base: Base, peer: Peer, text: string, args: string[]) { 11 | super(base, peer, text, args); 12 | this.opt = { 13 | command: ["help", "?"], 14 | description: "Shows available commands", 15 | cooldown: 5, 16 | ratelimit: 1, 17 | category: "`oBasic", 18 | usage: "/help", 19 | example: ["/help"], 20 | permission: [ROLE.BASIC, ROLE.SUPPORTER, ROLE.DEVELOPER], 21 | }; 22 | } 23 | 24 | private getRoleLevel(role: string): number { 25 | const roleLevels = { 26 | [ROLE.BASIC]: 1, 27 | [ROLE.SUPPORTER]: 2, 28 | [ROLE.DEVELOPER]: 3, 29 | }; 30 | return roleLevels[role] || 0; 31 | } 32 | 33 | public async execute(): Promise { 34 | if (this.args.length > 0) { 35 | const Class = CommandMap[this.args[0]]; 36 | 37 | if (!CommandMap[this.args[0]]) 38 | return this.peer.send( 39 | Variant.from( 40 | "OnConsoleMessage", 41 | "It seems that commands doesn't exist.", 42 | ), 43 | ); 44 | const cmd = new Class(this.base, this.peer, this.text, this.args); 45 | 46 | const dialog = new DialogBuilder() 47 | .defaultColor() 48 | .addLabelWithIcon(this.args[0] || "", "32", "small") 49 | .addSpacer("small") 50 | .addSmallText(`Description: ${cmd?.opt.description}`) 51 | .addSmallText(`Cooldown: ${cmd?.opt.cooldown}`) 52 | .addSmallText(`Ratelimit: ${cmd?.opt.ratelimit}`) 53 | .addSmallText( 54 | `Permissions: ${cmd?.opt.permission.length ? cmd.opt.permission : "None"}`, 55 | ) 56 | .addSmallText(`Usage: ${cmd?.opt.usage}`) 57 | .addSmallText(`Example: ${cmd?.opt.example.join(", ")}`) 58 | .endDialog("help_end", "", "Ok") 59 | .addQuickExit(); 60 | return this.peer.send(Variant.from("OnDialogRequest", dialog.str())); 61 | } 62 | const userRoleLevel = this.getRoleLevel(this.peer.data.role); 63 | 64 | // Filter and organize commands 65 | const commandsByCategory: Record = {}; 66 | 67 | Object.values(CommandMap).forEach((CommandClass) => { 68 | const cmd = new CommandClass(null, null, "", []); 69 | 70 | // Check if user has permission based on role hierarchy 71 | const hasPermission = cmd.opt.permission.some( 72 | (role) => this.getRoleLevel(role) <= userRoleLevel, 73 | ); 74 | 75 | if (hasPermission) { 76 | const category = cmd.opt.category || "Uncategorized"; 77 | if (!commandsByCategory[category]) { 78 | commandsByCategory[category] = []; 79 | } 80 | 81 | const commandString = `/${cmd.opt.command.join(", /")}`; 82 | // prevent any duplicates on commands 83 | if (!commandsByCategory[category].includes(commandString)) { 84 | commandsByCategory[category].push(commandString); 85 | } 86 | } 87 | }); 88 | 89 | // Build output message 90 | let message = "Available Commands: "; 91 | 92 | Object.entries(commandsByCategory).forEach(([category, commands]) => { 93 | message += `${category}: ${commands.join(", ")} `; 94 | }); 95 | 96 | this.peer.send(Variant.from("OnConsoleMessage", message)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /apps/server/src/world/tiles/DoorTile.ts: -------------------------------------------------------------------------------- 1 | import { Variant } from "growtopia.js"; 2 | import { 3 | BlockFlags, 4 | LockPermission, 5 | TileExtraTypes, 6 | TileFlags, 7 | } from "@growserver/const"; 8 | import type { Base } from "../../core/Base"; 9 | import { Peer } from "../../core/Peer"; 10 | import type { World } from "../../core/World"; 11 | import type { TileData } from "@growserver/types"; 12 | import { ExtendBuffer, DialogBuilder } from "@growserver/utils"; 13 | import { Tile } from "../Tile"; 14 | import { ItemDefinition } from "grow-items"; 15 | 16 | export class DoorTile extends Tile { 17 | public extraType = TileExtraTypes.DOOR; 18 | 19 | constructor( 20 | public base: Base, 21 | public world: World, 22 | public data: TileData, 23 | ) { 24 | super(base, world, data); 25 | } 26 | 27 | public async onPlaceForeground( 28 | peer: Peer, 29 | itemMeta: ItemDefinition, 30 | ): Promise { 31 | if (!(await super.onPlaceForeground(peer, itemMeta))) { 32 | return false; 33 | } 34 | 35 | // by default, it is public 36 | // in real growtopia server, they dont use this. But because im lazy, im gonna use TileFlags instead - Badewen 37 | this.data.flags |= TileFlags.TILEEXTRA | TileFlags.PUBLIC; 38 | this.data.door = { 39 | destination: "", 40 | id: "", 41 | label: "", 42 | }; 43 | 44 | return true; 45 | } 46 | 47 | public async onDestroy(peer: Peer): Promise { 48 | await super.onDestroy(peer); 49 | this.data.door = undefined; 50 | } 51 | 52 | public async serialize(dataBuffer: ExtendBuffer): Promise { 53 | await super.serialize(dataBuffer); 54 | const labelTotalSize = 2 + (this.data.door!.label ?? "").length; 55 | dataBuffer.grow(1 + labelTotalSize + 1); 56 | 57 | // using '!' because we are certain that it exists. Otherwise, crash. 58 | dataBuffer.writeU8(this.extraType); 59 | dataBuffer.writeString(this.data.door!.label ?? ""); 60 | // 0x8 = Locked 61 | dataBuffer.writeU8(this.data.flags & TileFlags.PUBLIC ? 0x0 : 0x8); 62 | } 63 | 64 | public async onWrench(peer: Peer): Promise { 65 | if (!(await super.onWrench(peer))) { 66 | this.onPlaceFail(peer); 67 | return false; 68 | } 69 | 70 | const itemMeta = this.base.items.metadata.items.get( 71 | this.data.fg.toString(), 72 | )!; 73 | const dialog = new DialogBuilder() 74 | .defaultColor() 75 | .addLabelWithIcon( 76 | `\`wEdit ${itemMeta.name}\`\``, 77 | itemMeta.id as number, 78 | "big", 79 | ) 80 | .addInputBox("label", "Label", this.data.door?.label, 100) 81 | .addInputBox("target", "Destination", this.data.door?.destination, 24) 82 | .addSmallText("Enter a Destination in this format: `2WORLDNAME:ID``") 83 | .addSmallText( 84 | "Leave `2WORLDNAME`` blank (:ID) to go to the door with `2ID`` in the `2Current World``.", 85 | ) 86 | .addInputBox("id", "ID", this.data.door?.id, 11) 87 | .addSmallText( 88 | "Set a unique `2ID`` to target this door as a Destination from another!", 89 | ) 90 | // i dont think following the real growtopia who is using checkbox_locked is worh it lol 91 | .addCheckbox( 92 | "checkbox_public", 93 | "Is open to public", 94 | this.data.flags & TileFlags.PUBLIC ? "selected" : "not_selected", 95 | ) 96 | .embed("tilex", this.data.x) 97 | .embed("tiley", this.data.y) 98 | .embed("itemID", itemMeta.id) 99 | .endDialog("door_edit", "Cancel", "OK") 100 | .str(); 101 | 102 | peer.send(Variant.from("OnDialogRequest", dialog)); 103 | return true; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/types/misc.d.ts: -------------------------------------------------------------------------------- 1 | import { PeerData } from "./structures/peer"; 2 | import { WorldData } from "./structures/world"; 3 | import { Collection, CooldownOptions } from "./index"; 4 | import { ItemsDatMeta } from "grow-items"; 5 | import { ItemsInfo } from "./structures/item-pages"; 6 | 7 | export interface CDNContent { 8 | version: string; 9 | uri: string; 10 | itemsDatName: string; 11 | } 12 | 13 | export interface StringOptions { 14 | id?: number; 15 | encoded?: boolean; 16 | } 17 | 18 | export interface ItemsData { 19 | hash: string; 20 | content: Buffer; 21 | metadata: ItemsDatMeta; 22 | wiki: ItemsInfo[]; 23 | } 24 | 25 | export interface Cache { 26 | peers: Collection; 27 | worlds: Collection; 28 | cooldown: Collection; 29 | } 30 | 31 | export interface MipMap { 32 | width: number; 33 | height: number; 34 | bufferLength: number; 35 | count: number; 36 | } 37 | 38 | export interface RTPack { 39 | type: string; 40 | version: number; 41 | reserved: number; 42 | compressedSize: number; 43 | decompressedSize: number; 44 | compressionType: number; 45 | reserved2: Int8Array; 46 | } 47 | 48 | export interface RTTXTR { 49 | type: string; 50 | version: number; 51 | reserved: number; 52 | width: number; 53 | height: number; 54 | format: number; 55 | originalWidth: number; 56 | originalHeight: number; 57 | isAlpha: number; 58 | isCompressed: number; 59 | reservedFlags: number; 60 | mipmap: MipMap; 61 | reserved2: Int32Array; 62 | } 63 | 64 | export interface CustomItemsConfig { 65 | assets: CustomItemsAssets[]; 66 | } 67 | 68 | export interface CustomItemsAssets { 69 | id: number; 70 | item: CustomItemsProps; 71 | storePath: string; 72 | } 73 | 74 | export interface CustomItemsProps { 75 | extraFile?: CustomItemsExtraFile; 76 | texture?: CustomItemsTexture; 77 | 78 | flags?: number; 79 | flagsCategory?: number; 80 | type?: number; 81 | materialType?: number; 82 | name?: string; 83 | visualEffectType?: number; 84 | flags2?: number; 85 | textureX?: number; 86 | textureY?: number; 87 | storageType?: number; 88 | isStripeyWallpaper?: number; 89 | collisionType?: number; 90 | breakHits?: number; 91 | resetStateAfter?: number; 92 | bodyPartType?: number; 93 | blockType?: number; 94 | growTime?: number; 95 | rarity?: number; 96 | maxAmount?: number; 97 | audioVolume?: number; 98 | petName?: string; 99 | petPrefix?: string; 100 | petSuffix?: string; 101 | petAbility?: string; 102 | seedBase?: number; 103 | seedOverlay?: number; 104 | treeBase?: number; 105 | treeLeaves?: number; 106 | seedColor?: number; 107 | seedOverlayColor?: number; 108 | isMultiFace?: number; 109 | isRayman?: number; 110 | extraOptions?: string; 111 | texture2?: string; 112 | extraOptions2?: string; 113 | punchOptions?: string; 114 | 115 | extraBytes?: Buffer; 116 | 117 | // new options 118 | ingredient?: number; 119 | flags3?: number; 120 | flags4?: number; 121 | bodyPart?: Buffer; 122 | flags5?: number; 123 | extraTexture?: string; 124 | itemRenderer?: string; 125 | unknownInt1?: number; // NOTE: not sure what this does 126 | unknownBytes1?: Buffer; // NOTE: not sure what this does 127 | extraFlags1?: number; // NOTE: not sure what this does 128 | extraHash1?: number; // NOTE: not sure what this does 129 | unknownBytes2?: Buffer; // NOTE: not sure what this does 130 | } 131 | 132 | export interface CustomItemsExtraFile { 133 | fileName: string; 134 | pathAsset: string; 135 | pathResult: string; 136 | } 137 | 138 | export interface CustomItemsTexture { 139 | fileName: string; 140 | pathAsset: string; 141 | pathResult: string; 142 | } 143 | --------------------------------------------------------------------------------