@starktma/minecraft-utils 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Database Management in Minecraft: TypeScript Edition
2
+
3
+ PropertyDatabase is a TypeScript-based database management system designed for use within Minecraft. The system consists of two main classes: `DatabaseManager` and `SimpleDatabase`, which handle JSON database operations in the Minecraft world.
4
+
5
+ ## Features
6
+
7
+ - **Entity & World Targeting**: Ability to target either an entity or the Minecraft world for storing data.
8
+ - **JSON Database Operations**: Supports basic CRUD (Create, Read, Update, Delete) operations for JSON databases.
9
+ - **Custom Object Management**: Facilitates storing and managing custom objects with unique IDs.
10
+
11
+ ## Installation
12
+
13
+ 1. Ensure you have the Minecraft server set up with the appropriate scripting API support.
14
+ 2. Install the PropertyDatabase module `@starktma/minecraft-utils` using your preferred method (e.g., npm, yarn).
15
+ 3. Place the PropertyDatabase module in your server's script directory or in the assets/javascript directory if you're using Anvil.
16
+
17
+ ## Usage
18
+
19
+ ### DatabaseManager Class
20
+
21
+ Handles JSON databases in Minecraft's world properties. Can target an entity or the world itself.
22
+
23
+ #### Methods:
24
+
25
+ - `hasJSONDatabase(databaseName: string)`: Checks if a database exists.
26
+ - `addJSONDatabase(databaseName: string, database: object)`: Adds a new database.
27
+ - `removeJSONDatabase(databaseName: string)`: Removes a database.
28
+ - `getJSONDatabase(databaseName: string)`: Retrieves a database.
29
+
30
+ ### SimpleDatabase Class
31
+
32
+ A base class for managing databases of custom objects.
33
+
34
+ #### Methods:
35
+
36
+ - `addObject(object: SimpleObject)`: Adds a new object to the database.
37
+ - `updateObject(object: SimpleObject)`: Updates an existing object.
38
+ - `hasObject(id: string)`: Checks if an object exists.
39
+ - `getObject(id: string)`: Retrieves an object by ID.
40
+ - `removeObject(id: string)`: Removes an object.
41
+ - `getAllObjects()`: Retrieves all objects.
42
+ - `eraseAllObjects()`: Clears the database.
43
+ - `forEach(callback: Function)`: Iterates over objects.
44
+
45
+ ### SimpleObject Interface
46
+
47
+ A basic interface for objects managed by `SimpleDatabase`. Must include an `id` property.
48
+
49
+ ## NOTE
50
+
51
+ The `DatabaseManager` class is designed to handle JSON databases in Minecraft's world properties and cannot be used directly. Its main job is to convert your database to string formats that can be stored on dynamic properties and back.
52
+
53
+ The `DatabaseManager` class is responsible for deconstructing and reconstructing the database from JSON strings to work around the dynamic property size limits in Minecraft [`32767`].
54
+
55
+ The `SimpleDatabase` class uses the `DatabaseManager` to manage custom objects. It provides methods to add, update, retrieve, and remove objects, as well as to iterate over all objects in the database.
56
+
57
+ The `SimpleDatabase` is intended to be inherited by custom object classes. It requires an `id` property to uniquely identify each object and can be extended with additional properties as needed.
58
+
59
+ The `SimpleObject` interface defines the structure of objects that can be managed by `SimpleDatabase`. It includes an `id` property, which is essential for identifying objects within the database, and can be extended with additional properties to structure your own database objects.
60
+
61
+ ## Examples
62
+
63
+ ### Creating a Custom Database
64
+
65
+ ```typescript
66
+ class MyDatabase extends SimpleDatabase {
67
+ constructor() {
68
+ super("my_database");
69
+ }
70
+
71
+ addObject(object: MyObject) {
72
+ super.addObject(object);
73
+ }
74
+
75
+ // Additional custom methods...
76
+ }
77
+ ```
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/database/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS,sBAAsB,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/database/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAgB,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AA0I5C;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,cAAc,CAAC,CAAC,SAAS,YAAY;IACjD,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,OAAO,CAAM;IAErB,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC;IAE/B;;;;OAIG;IACH,SAAS,aAAa,YAAY,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS;IAYvE;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAIpB;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAIjB;;;OAGG;IACH,SAAS,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI;IAK1B;;;;OAIG;IACH,YAAY,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI;IAO7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAI9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAIpC;;;OAGG;IACH,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAK9B;;;OAGG;IACH,aAAa,IAAI,CAAC,EAAE;IAIpB;;OAEG;IACH,eAAe,IAAI,IAAI;IAKvB;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;CAGtE"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../../src/database/interfaces.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;CACX"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/math/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAE5C;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,CAIhF;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAmBlG;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG9C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,GAAE,MAAY,GAAG,OAAO,CAEvF;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAExE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/math/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEvD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,GAAG,MAAM,CAIhF;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,GAAE,OAAe,GAAG,GAAG,GAAG,IAAI,CAsB9E;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAErE;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG9C;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,GAAE,MAAY,GAAG,OAAO,CAEvF;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAExE"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/minecraft/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAS,MAAM,mBAAmB,CAAC;AAG1E,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,WA4BnG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,OAAO,EAAE,GAAG,QAO3E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/minecraft/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,MAAM,EACN,MAAM,EACN,KAAK,EACL,OAAO,EAEP,iBAAiB,EACjB,OAAO,EAOP,MAAM,mBAAmB,CAAC;AAG3B,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,KAAK,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,WA4BnG;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,OAAO,EAAE,GAAG,QAO3E;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKnD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,GAAE,MAAU,GAAG,OAAO,CAUjG;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAQ1F;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAapF;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAwC1D;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,GAAE,OAAe,GAAG,MAAM,CAuD3F"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/player-event/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,MAAM,EAEN,yBAAyB,EACzB,wBAAwB,EAGxB,SAAS,EACT,MAAM,mBAAmB,CAAC;AAE3B,KAAK,aAAa,CAAC,CAAC,GAAG,YAAY,IAAI,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;AAC3D,KAAK,eAAe,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAEnD,aAAK,kBAAkB;IACtB,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,UAAU,eAAe;IACzB,SAAS,cAAc;IACvB,QAAQ,aAAa;CACrB;AAED,cAAM,UAAU,CAAC,CAAC,GAAG,YAAY;IAChC,OAAO,CAAC,SAAS,CAAkD;;IASnE,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC;IAOxC,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC;IAQpD,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC;CASrD;AAED,cAAM,YAAY;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,SAAK;IACd,QAAQ,SAAK;IACb,SAAS,SAAK;gBAEF,MAAM,EAAE,MAAM;IAK1B,IAAI,mBAAmB,IACoD,yBAAyB,CACnG;IAED,IAAI,kBAAkB,IACoD,wBAAwB,CACjG;IAED,eAAe,IAAI,SAAS,GAAG,SAAS;IAIxC,KAAK;IAML,IAAI;CAmBJ;AAED,cAAM,aAAa,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY;IACxD,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAoC;IAElD,SAAS;IAwBT,SAAS,CAAC,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC;IAIhD,SAAS,CAAC,EAAE,EAAE,MAAM;IAIpB,YAAY,CAAC,EAAE,EAAE,MAAM;IAIvB,SAAS,CAAC,MAAM,EAAE,MAAM;IAKxB,cAAc,CAAC,OAAO,EAAE,MAAM;;;IAM9B,SAAS,CAAC,OAAO,EAAE,MAAM;IAIzB,IAAI;CAWJ;AAED,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starktma/minecraft-utils",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -23,6 +23,14 @@
23
23
  "./minecraft": {
24
24
  "import": "./dist/minecraft/index.js",
25
25
  "types": "./dist/minecraft/index.d.ts"
26
+ },
27
+ "./database": {
28
+ "import": "./dist/database/index.js",
29
+ "types": "./dist/database/index.d.ts"
30
+ },
31
+ "./player-events": {
32
+ "import": "./dist/player-event/index.js",
33
+ "types": "./dist/player-event/index.d.ts"
26
34
  }
27
35
  },
28
36
  "publishConfig": {
@@ -0,0 +1 @@
1
+ export const NAMESPACE = 'inventory-manager';
@@ -0,0 +1,271 @@
1
+ import { Entity, world, World } from "@minecraft/server";
2
+ import { SimpleObject } from "./interfaces";
3
+
4
+ //@ts-ignore
5
+ import { NAMESPACE } from "./constants";
6
+
7
+ /**
8
+ * DatabaseManager is a class that manages databases stored in Minecraft's world properties.
9
+ * Currently only supports JSON databases.
10
+ */
11
+ class DatabaseManager {
12
+ private static readonly DYNAMIC_PROP_MAX_LENGTH = 32767;
13
+ private static readonly CHUNK_KEY = "__SPLIT__";
14
+
15
+ private target: Entity | World;
16
+
17
+ constructor(target: Entity | undefined) {
18
+ if (target instanceof Entity) {
19
+ this.target = target;
20
+ } else {
21
+ this.target = world;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Checks if a JSON database with the given name exists.
27
+ * @param databaseName The name of the database.
28
+ * @returns True if the database exists, false otherwise.
29
+ */
30
+ hasJSONDatabase(databaseName: string) {
31
+ return this.target.getDynamicProperty(NAMESPACE + ":" + databaseName) !== undefined;
32
+ }
33
+
34
+ /**
35
+ * Adds a new JSON database with the given name and data.
36
+ * @param databaseName The name of the database.
37
+ * @param database The data to be stored in the database.
38
+ */
39
+ addJSONDatabase(databaseName: string, database: object) {
40
+ const propertyName = `${NAMESPACE}:${databaseName}`;
41
+ const jsonString = JSON.stringify(database);
42
+ const existingProp = this.target.getDynamicProperty(propertyName) as string | undefined;
43
+ let existingChunks = 0;
44
+
45
+ if (existingProp) {
46
+ try {
47
+ const propObj = JSON.parse(existingProp);
48
+ if (propObj && typeof propObj === "object" && DatabaseManager.CHUNK_KEY in propObj) {
49
+ existingChunks = propObj[DatabaseManager.CHUNK_KEY];
50
+ }
51
+ } catch {}
52
+ }
53
+ if (jsonString.length <= DatabaseManager.DYNAMIC_PROP_MAX_LENGTH || jsonString.length === 0) {
54
+ if (existingChunks > 0) {
55
+ for (let i = 0; i < existingChunks; i++) {
56
+ const partName = `${propertyName}_${i}`;
57
+ this.target.setDynamicProperty(partName, undefined);
58
+ }
59
+ }
60
+ this.target.setDynamicProperty(propertyName, jsonString);
61
+ } else {
62
+ const chunkSize = DatabaseManager.DYNAMIC_PROP_MAX_LENGTH;
63
+ const chunkCount = Math.ceil(jsonString.length / chunkSize);
64
+ if (existingChunks > 0) {
65
+ for (let i = 0; i < existingChunks; i++) {
66
+ const oldPartName = `${propertyName}_${i}`;
67
+ this.target.setDynamicProperty(oldPartName, undefined);
68
+ }
69
+ }
70
+ for (let i = 0; i < chunkCount; i++) {
71
+ const start = i * chunkSize;
72
+ const end = start + chunkSize;
73
+ const chunk = jsonString.slice(start, end);
74
+ const partName = `${propertyName}_${i}`;
75
+ this.target.setDynamicProperty(partName, chunk);
76
+ }
77
+ const meta = { [DatabaseManager.CHUNK_KEY]: chunkCount };
78
+ this.target.setDynamicProperty(propertyName, JSON.stringify(meta));
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Removes a JSON database with the given name.
84
+ * @param databaseName The name of the database.
85
+ */
86
+ removeJSONDatabase(databaseName: string) {
87
+ const propertyName = `${NAMESPACE}:${databaseName}`;
88
+ const propString = this.target.getDynamicProperty(propertyName) as string | undefined;
89
+ if (propString !== undefined) {
90
+ try {
91
+ const propObj = JSON.parse(propString);
92
+ if (propObj && typeof propObj === "object" && DatabaseManager.CHUNK_KEY in propObj) {
93
+ const chunkCount = propObj[DatabaseManager.CHUNK_KEY];
94
+ for (let i = 0; i < chunkCount; i++) {
95
+ const partName = `${propertyName}_${i}`;
96
+ this.target.setDynamicProperty(partName, undefined);
97
+ }
98
+ }
99
+ } catch {}
100
+ this.target.setDynamicProperty(propertyName, undefined);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Retrieves a JSON database with the given name.
106
+ * @param databaseName The name of the database.
107
+ * @returns The data stored in the database.
108
+ * @throws An error if the database does not exist.
109
+ */
110
+ getJSONDatabase(databaseName: string) {
111
+ const propertyName = `${NAMESPACE}:${databaseName}`;
112
+ const propString = this.target.getDynamicProperty(propertyName) as string | undefined;
113
+ if (propString === undefined) {
114
+ throw new Error("Database does not exist");
115
+ }
116
+ try {
117
+ const propObj = JSON.parse(propString);
118
+ if (propObj && typeof propObj === "object" && DatabaseManager.CHUNK_KEY in propObj) {
119
+ const chunkCount: number = propObj[DatabaseManager.CHUNK_KEY];
120
+ let combined = "";
121
+ for (let i = 0; i < chunkCount; i++) {
122
+ const partName = `${propertyName}_${i}`;
123
+ const part = this.target.getDynamicProperty(partName) as string | undefined;
124
+ if (typeof part === "string") {
125
+ combined += part;
126
+ } else {
127
+ combined += "";
128
+ }
129
+ }
130
+ return JSON.parse(combined);
131
+ } else {
132
+ return JSON.parse(propString);
133
+ }
134
+ } catch {
135
+ throw new Error("Failed to parse database JSON");
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * SimpleDatabase is a base class for databases that store custom objects with an id property.
142
+ * It provides methods for adding, updating, removing and retrieving objects from the database.
143
+ * A singleton pattern is used to ensure that only one instance of the database exists.
144
+ *
145
+ * @example
146
+ * class MyDatabase extends SimpleDatabase<PlayerObject> {
147
+ * protected static instance: MyDatabase;
148
+ * constructor() {
149
+ * super("myDatabase", undefined);
150
+ * }
151
+ *
152
+ * static getInstance(): MyDatabase {
153
+ * if (!MyDatabase.instance) {
154
+ * MyDatabase.instance = new MyDatabase();
155
+ * }
156
+ * return MyDatabase.instance;
157
+ * }
158
+ */
159
+ export class SimpleDatabase<T extends SimpleObject> {
160
+ private mainDB: DatabaseManager;
161
+ private localDB: T[];
162
+
163
+ protected databaseName: string;
164
+
165
+ /**
166
+ * The constructor initializes the local database and syncs it with the main database.
167
+ * @param databaseName The name of the database.
168
+ * @param target The target entity to store the database in. If undefined, the database is stored in the world.
169
+ */
170
+ protected constructor(databaseName: string, target?: Entity | undefined) {
171
+ this.mainDB = new DatabaseManager(target);
172
+ this.databaseName = databaseName;
173
+
174
+ if (this.mainDB.hasJSONDatabase(this.databaseName)) {
175
+ this.localDB = this.getMainDB();
176
+ } else {
177
+ this.localDB = [];
178
+ this.updateMainDB();
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Updates the main database with the local database.
184
+ * This method is called automatically when an object is added, updated or removed.
185
+ * @private
186
+ */
187
+ private updateMainDB() {
188
+ this.mainDB.addJSONDatabase(this.databaseName, this.localDB);
189
+ }
190
+
191
+ /**
192
+ * Retrieves the main database.
193
+ * @private
194
+ * @returns The main database.
195
+ */
196
+ private getMainDB() {
197
+ return this.mainDB.getJSONDatabase(this.databaseName);
198
+ }
199
+
200
+ /**
201
+ * Adds an object to the local database and updates the main database.
202
+ * @param object The object to be added.
203
+ */
204
+ addObject(object: T): void {
205
+ this.localDB.push(object);
206
+ this.updateMainDB();
207
+ }
208
+
209
+ /**
210
+ * Updates an object in the local database and the main database.
211
+ * If the object does not exist, it is added.
212
+ * @param object The object to be updated.
213
+ */
214
+ updateObject(object: T): void {
215
+ if (this.hasObject(object.id)) {
216
+ this.removeObject(object.id);
217
+ }
218
+ this.addObject(object);
219
+ }
220
+
221
+ /**
222
+ * Checks if an object with the given id exists in the local database.
223
+ * @param id The id of the object.
224
+ * @returns True if the object exists, false otherwise.
225
+ */
226
+ hasObject(id: string): boolean {
227
+ return this.localDB.map((object) => object.id).includes(id);
228
+ }
229
+
230
+ /**
231
+ * Retrieves an object with the given id from the local database.
232
+ * @param id The id of the object.
233
+ * @returns The object if it exists, undefined otherwise.
234
+ */
235
+ getObject(id: string): T | undefined {
236
+ return this.localDB.filter((object) => object.id === id)[0];
237
+ }
238
+
239
+ /**
240
+ * Removes an object with the given id from the local database and updates the main database.
241
+ * @param id The id of the object.
242
+ */
243
+ removeObject(id: string): void {
244
+ this.localDB = this.localDB.filter((object) => object.id !== id);
245
+ this.updateMainDB();
246
+ }
247
+
248
+ /**
249
+ * Retrieves all objects from the local database.
250
+ * @returns An array of all objects in the local database.
251
+ */
252
+ getAllObjects(): T[] {
253
+ return this.localDB;
254
+ }
255
+
256
+ /**
257
+ * Removes all objects from the local database and updates the main database.
258
+ */
259
+ eraseAllObjects(): void {
260
+ this.localDB = [];
261
+ this.updateMainDB();
262
+ }
263
+
264
+ /**
265
+ * Iterates over all objects in the local database.
266
+ * @param callback The function to be called for each object.
267
+ */
268
+ forEach(callback: (object: SimpleObject, index: number) => void): void {
269
+ this.localDB.forEach((object, index) => callback(object, index));
270
+ }
271
+ }
@@ -0,0 +1,4 @@
1
+ import { SimpleDatabase } from "./database";
2
+ import { SimpleObject } from "./interfaces";
3
+
4
+ export { SimpleDatabase, SimpleObject };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * SimpleObject is an interface for objects with an id property.
3
+ */
4
+ export interface SimpleObject {
5
+ id: string;
6
+ }
package/src/math/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Vector3 } from "@minecraft/server";
1
+ import { RGB, RGBA, Vector3 } from "@minecraft/server";
2
2
 
3
3
  /**
4
4
  * Converts an angle in degrees to radians.
@@ -18,7 +18,7 @@ export function toRadians(degrees: number): number {
18
18
  */
19
19
  export function calculateDistance(position1: Vector3, position2: Vector3): number {
20
20
  return Math.sqrt(
21
- Math.sqrt(Math.pow(position1.x - position2.x, 2) + Math.pow(position1.y - position2.y, 2)) + Math.pow(position1.z - position2.z, 2)
21
+ Math.pow(position1.x - position2.x, 2) + Math.pow(position1.y - position2.y, 2) + Math.pow(position1.z - position2.z, 2)
22
22
  );
23
23
  }
24
24
 
@@ -39,7 +39,7 @@ export function toDegrees(radians: number): number {
39
39
  * @returns An object containing the red, green, blue, and alpha components as numbers between 0 and 1.
40
40
  * @throws Error if the hex string is invalid.
41
41
  */
42
- export function hexToRgba(hex: string): { red: number; green: number; blue: number; alpha: number } {
42
+ export function hexToRgba(hex: string, stripAlpha: boolean = false): RGB | RGBA {
43
43
  if (!/^#([a-fA-F0-9]{4}|[a-fA-F0-9]{8}|[a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/.test(hex)) {
44
44
  throw new Error("Invalid hex color");
45
45
  }
@@ -57,6 +57,9 @@ export function hexToRgba(hex: string): { red: number; green: number; blue: numb
57
57
  const blue = parseInt(normalized.substring(4, 6), 16) / 255;
58
58
  const alpha = (normalized.length === 8 ? parseInt(normalized.substring(6, 8), 16) : 255) / 255;
59
59
 
60
+ if (stripAlpha) {
61
+ return { red, green, blue };
62
+ }
60
63
  return { red, green, blue, alpha };
61
64
  }
62
65
 
@@ -1,5 +1,19 @@
1
- import { Player, Entity, Block, Vector3, world } from "@minecraft/server";
2
- import { calculateDistance, toUnsigned } from "../math";
1
+ import {
2
+ Player,
3
+ Entity,
4
+ Block,
5
+ Vector3,
6
+ world,
7
+ StructureRotation,
8
+ Vector2,
9
+ BlockInventoryComponent,
10
+ BlockPermutation,
11
+ EntityEquippableComponent,
12
+ EntityInventoryComponent,
13
+ EquipmentSlot,
14
+ ItemStack,
15
+ } from "@minecraft/server";
16
+ import { calculateDistance, toRadians, toSigned, toUnsigned } from "../math";
3
17
 
4
18
  export function getBlocksInASphere(centerBlock: Block | Entity, radius: number, innerRadius?: number) {
5
19
  if (centerBlock) {
@@ -37,7 +51,7 @@ export function displayActionbar(player: Player | undefined, ...message: any) {
37
51
  let selector;
38
52
  target = player && player.isValid ? player : world.getDimension("overworld");
39
53
  selector = player ? `@s` : `@a`;
40
- target.runCommand(`title @s actionbar ${JSON.stringify(text)}`);
54
+ target.runCommand(`title ${selector} actionbar ${JSON.stringify(text)}`);
41
55
  }
42
56
 
43
57
  /**
@@ -46,9 +60,11 @@ export function displayActionbar(player: Player | undefined, ...message: any) {
46
60
  * @param angle - The angle in degrees to be snapped.
47
61
  * @returns The snapped unsigned angle in degrees.
48
62
  */
49
- export function snapRotationToGrid(angle: number): number {
63
+ export function snapYawToGrid(angle: number): number {
50
64
  const gridSize = 90;
51
- return Math.round(toUnsigned(angle) / gridSize) * gridSize;
65
+ // Round to nearest grid step, then normalize into [0, 360)
66
+ const snapped = Math.round(toUnsigned(angle) / gridSize) * gridSize;
67
+ return ((snapped % 360) + 360) % 360;
52
68
  }
53
69
 
54
70
  /**
@@ -57,6 +73,138 @@ export function snapRotationToGrid(angle: number): number {
57
73
  * @param v - The vector to be snapped.
58
74
  * @returns A new vector with each component rounded to the nearest integer.
59
75
  */
60
- export function snapLocationToGrid(v: Vector3): Vector3 {
61
- return { x: Math.round(v.x), y: Math.floor(v.y), z: Math.round(v.z) };
76
+ export function snapLocationToGrid(location: Vector3, yaw: Vector2, gridSize: number = 1): Vector3 {
77
+ const snappedYaw = snapYawToGrid(yaw.y);
78
+ const dx = Math.round(Math.sin(snappedYaw));
79
+ const dz = Math.round(Math.cos(snappedYaw));
80
+
81
+ return {
82
+ x: Math.floor(location.x / gridSize) * gridSize,
83
+ y: Math.floor(location.y / gridSize) * gridSize,
84
+ z: Math.floor(location.z / gridSize) * gridSize,
85
+ };
86
+ }
87
+
88
+ export function getStructureRotationEnum(angle: number, offset?: number): StructureRotation {
89
+ const diff = snapYawToGrid(toUnsigned(angle - (offset ?? 0)));
90
+ console.log(diff);
91
+
92
+ if (Math.abs(diff - 90) < Number.EPSILON) return StructureRotation.Rotate90;
93
+ if (Math.abs(diff - 180) < Number.EPSILON) return StructureRotation.Rotate180;
94
+ if (Math.abs(diff - 270) < Number.EPSILON) return StructureRotation.Rotate270;
95
+ return StructureRotation.None;
96
+ }
97
+
98
+ export function getRelativeMovementDirection(player: Player, round: boolean): Vector3 {
99
+ const playerYaw = toRadians(player.getRotation().y);
100
+ const { sin, cos } = { sin: Math.sin(playerYaw), cos: Math.cos(playerYaw) };
101
+
102
+ const playerMovement = player.inputInfo.getMovementVector();
103
+
104
+ const dx = -sin * playerMovement.y + cos * playerMovement.x;
105
+ const dz = cos * playerMovement.y + sin * playerMovement.x;
106
+
107
+ if (round) {
108
+ return { x: Math.round(dx), y: 0, z: Math.round(dz) };
109
+ }
110
+ return { x: dx, y: 0, z: dz };
111
+ }
112
+
113
+ export function restoreInventory(entity: Entity, id: string) {
114
+ const blockLocation = entity.dimension.getBlockFromRay(entity.location, { x: 0, y: -1, z: 0 })!.block.location;
115
+ blockLocation.y++;
116
+ const blockLocation2 = { ...blockLocation, y: blockLocation.y + 1 };
117
+
118
+ const savedStructure = world.structureManager.get(id);
119
+ if (savedStructure) world.structureManager.place(savedStructure, entity.dimension, blockLocation);
120
+ world.structureManager.delete(id);
121
+
122
+ const block = entity.dimension.getBlock(blockLocation)!;
123
+ const block2 = entity.dimension.getBlock(blockLocation2)!;
124
+
125
+ const equipment = entity.getComponent(EntityEquippableComponent.componentId) as EntityEquippableComponent;
126
+ const invComponent = entity.getComponent(EntityInventoryComponent.componentId) as EntityInventoryComponent;
127
+ const blockInventory = block.getComponent(BlockInventoryComponent.componentId) as BlockInventoryComponent;
128
+ const blockInventory2 = block2.getComponent(BlockInventoryComponent.componentId) as BlockInventoryComponent;
129
+
130
+ const slots = Object.values(EquipmentSlot);
131
+ slots.forEach((slot) => {
132
+ equipment.setEquipment(slot, blockInventory.container!.getItem(slots.indexOf(slot))!);
133
+ });
134
+
135
+ Array(41)
136
+ .fill(0)
137
+ .forEach((_, i) => {
138
+ if (i < invComponent.container!.size) {
139
+ if (i < 9) {
140
+ // First chest: starts filling from slot 5
141
+ blockInventory.container!.moveItem(i + 5, i, invComponent.container!);
142
+ } else {
143
+ // Second chest: adjusts index for 0-26 range starting from slot 0
144
+ blockInventory2.container!.moveItem(i - 9, i, invComponent.container!);
145
+ }
146
+ }
147
+ });
148
+
149
+ blockInventory.container!.clearAll();
150
+ blockInventory2.container!.clearAll();
151
+ block.setPermutation(BlockPermutation.resolve("minecraft:air"));
152
+ block2.setPermutation(BlockPermutation.resolve("minecraft:air"));
153
+ }
154
+
155
+ export function saveInventory(entity: Entity, id: string, clearAll: boolean = false): number {
156
+ const blockLocation = entity.dimension.getTopmostBlock(entity.location)!.location;
157
+ blockLocation.y++;
158
+ const blockLocation2 = { ...blockLocation, y: blockLocation.y + 1 };
159
+
160
+ const block = entity.dimension.getBlock(blockLocation)!;
161
+ const block2 = entity.dimension.getBlock(blockLocation2)!;
162
+
163
+ block.setPermutation(BlockPermutation.resolve("minecraft:chest"));
164
+ block2.setPermutation(BlockPermutation.resolve("minecraft:chest"));
165
+
166
+ const equipment = entity.getComponent(EntityEquippableComponent.componentId) as EntityEquippableComponent;
167
+ const invComponent = entity.getComponent(EntityInventoryComponent.componentId) as EntityInventoryComponent;
168
+ const blockInventory = block.getComponent(BlockInventoryComponent.componentId) as BlockInventoryComponent;
169
+ const blockInventory2 = block2.getComponent(BlockInventoryComponent.componentId) as BlockInventoryComponent;
170
+
171
+ const itemCount = invComponent.container!.size - invComponent.container!.emptySlotsCount;
172
+
173
+ Object.values(EquipmentSlot).forEach((slot) => {
174
+ if (clearAll) {
175
+ equipment.setEquipment(slot);
176
+ }
177
+ blockInventory.container!.addItem(equipment.getEquipment(slot) ?? new ItemStack("minecraft:air", 1));
178
+ });
179
+
180
+ if (clearAll) {
181
+ }
182
+
183
+ Array(41)
184
+ .fill(0)
185
+ .forEach((_, i) => {
186
+ if (i < invComponent.container!.size) {
187
+ if (i < 9) {
188
+ // First chest: starts filling from slot 5
189
+ invComponent.container!.moveItem(i, 5 + i, blockInventory.container!);
190
+ } else {
191
+ // Second chest: adjusts index for 0-26 range starting from slot 0
192
+ invComponent.container!.moveItem(i, i - 9, blockInventory2.container!);
193
+ }
194
+ if (clearAll) {
195
+ invComponent.container!.setItem(i);
196
+ }
197
+ }
198
+ });
199
+
200
+ world.structureManager.delete(id);
201
+ world.structureManager.createFromWorld(id, entity.dimension, blockLocation, blockLocation2);
202
+
203
+ blockInventory.container!.clearAll();
204
+ blockInventory2.container!.clearAll();
205
+
206
+ block.setPermutation(BlockPermutation.resolve("minecraft:air"));
207
+ block2.setPermutation(BlockPermutation.resolve("minecraft:air"));
208
+
209
+ return itemCount;
62
210
  }
@@ -0,0 +1,184 @@
1
+ import {
2
+ system,
3
+ world,
4
+ Player,
5
+ ButtonState,
6
+ EntityEquippableComponent,
7
+ EntityInventoryComponent,
8
+ EquipmentSlot,
9
+ InputButton,
10
+ ItemStack,
11
+ } from "@minecraft/server";
12
+
13
+ type EventCallback<T = CustomPlayer> = (player: T) => void;
14
+ type PlayerEventType = CustomPlayerEvents | string;
15
+
16
+ enum CustomPlayerEvents {
17
+ Tick = "tick",
18
+ JumpStart = "jumpStart",
19
+ JumpHold = "jumpHold",
20
+ JumpEnd = "jumpEnd",
21
+ SneakStart = "sneakStart",
22
+ SneakHold = "sneakHold",
23
+ SneakEnd = "sneakEnd",
24
+ }
25
+
26
+ class EventGroup<T = CustomPlayer> {
27
+ private callbacks = new Map<PlayerEventType, EventCallback<T>[]>();
28
+
29
+ constructor() {
30
+ // Initialize with core events
31
+ Object.values(CustomPlayerEvents).forEach((event) => {
32
+ this.callbacks.set(event, []);
33
+ });
34
+ }
35
+
36
+ private run(callbacks: EventCallback<T>[], player: T) {
37
+ for (const cb of callbacks) cb(player);
38
+ }
39
+
40
+ trigger(type: PlayerEventType, player: T) {
41
+ const callbacks = this.callbacks.get(type);
42
+ if (callbacks) {
43
+ this.run(callbacks, player);
44
+ }
45
+ }
46
+
47
+ on(type: PlayerEventType, callback: EventCallback<T>) {
48
+ if (!this.callbacks.has(type)) {
49
+ this.callbacks.set(type, []);
50
+ }
51
+ this.callbacks.get(type)!.push(callback);
52
+ }
53
+
54
+ // Simple way to remove listeners
55
+ off(type: PlayerEventType, callback: EventCallback<T>) {
56
+ const callbacks = this.callbacks.get(type);
57
+ if (callbacks) {
58
+ const index = callbacks.indexOf(callback);
59
+ if (index > -1) {
60
+ callbacks.splice(index, 1);
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ class CustomPlayer {
67
+ player: Player;
68
+ stateTick = 0;
69
+ JumpTick = 0;
70
+ SneakTick = 0;
71
+
72
+ constructor(player: Player) {
73
+ this.player = player;
74
+ this.reset();
75
+ }
76
+
77
+ get equippableComponent() {
78
+ return this.player.getComponent(EntityEquippableComponent.componentId) as EntityEquippableComponent;
79
+ }
80
+
81
+ get inventoryComponent() {
82
+ return this.player.getComponent(EntityInventoryComponent.componentId) as EntityInventoryComponent;
83
+ }
84
+
85
+ getEquippedItem(): ItemStack | undefined {
86
+ return this.equippableComponent.getEquipment(EquipmentSlot.Mainhand);
87
+ }
88
+
89
+ reset() {
90
+ this.stateTick = 0;
91
+ this.JumpTick = 0;
92
+ this.player.camera.clear();
93
+ }
94
+
95
+ tick() {
96
+ this.stateTick++;
97
+
98
+ if (this.player.inputInfo.getButtonState(InputButton.Jump) === ButtonState.Pressed) {
99
+ this.JumpTick++;
100
+ } else if (this.JumpTick > 0) {
101
+ this.JumpTick = -1;
102
+ } else {
103
+ this.JumpTick = 0;
104
+ }
105
+
106
+ if (this.player.inputInfo.getButtonState(InputButton.Sneak) === ButtonState.Pressed) {
107
+ this.SneakTick++;
108
+ } else if (this.SneakTick > 0) {
109
+ this.SneakTick = -1;
110
+ } else {
111
+ this.SneakTick = 0;
112
+ }
113
+ }
114
+ }
115
+
116
+ class PlayerManager<T extends CustomPlayer = CustomPlayer> {
117
+ private players = new Map<string, T>();
118
+ private events = new Map<string, EventGroup<T>>();
119
+
120
+ protected constructor() {
121
+ world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => {
122
+ const id = player.id;
123
+ const existing = this.getPlayer(id);
124
+
125
+ if (initialSpawn || !existing) {
126
+ this.addPlayer(player);
127
+ } else {
128
+ existing.reset();
129
+ }
130
+ });
131
+
132
+ world.afterEvents.playerLeave.subscribe(({ playerId }) => {
133
+ this.removePlayer(playerId);
134
+ });
135
+
136
+ world.afterEvents.worldLoad.subscribe(() => {
137
+ world.getAllPlayers().forEach((player) => this.addPlayer(player));
138
+ system.runInterval(() => {
139
+ this.tick();
140
+ });
141
+ });
142
+ }
143
+
144
+ protected createPlayerManager(player: Player): T {
145
+ return new CustomPlayer(player) as T;
146
+ }
147
+
148
+ getPlayer(id: string) {
149
+ return this.players.get(id);
150
+ }
151
+
152
+ removePlayer(id: string) {
153
+ this.players.delete(id);
154
+ }
155
+
156
+ addPlayer(player: Player) {
157
+ if (this.players.has(player.id)) return;
158
+ this.players.set(player.id, this.createPlayerManager(player));
159
+ }
160
+
161
+ registerEvents(eventID: string) {
162
+ const eventGroup = new EventGroup<T>();
163
+ this.events.set(eventID, eventGroup);
164
+ return { event: eventGroup };
165
+ }
166
+
167
+ getEvents(eventID: string) {
168
+ return this.events.get(eventID);
169
+ }
170
+
171
+ tick() {
172
+ for (const manager of this.players.values()) {
173
+ const { player } = manager;
174
+ if (!player.isValid) continue;
175
+
176
+ manager.tick();
177
+ this.events.forEach((eventGroup) => {
178
+ eventGroup.trigger(CustomPlayerEvents.Tick, manager);
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ export { CustomPlayerEvents, CustomPlayer, PlayerManager };