├── .altv
├── .github
└── FUNDING.yml
├── .gitignore
├── README.md
├── client
├── entitysync.js
├── index.js
└── textlabel-example.js
├── postinstall.js
├── resource.cfg
└── server
├── entitysync.js
├── index.js
└── textlabel-example.js
/.altv:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "postinstall",
4 | "file": "postinstall.js"
5 | }
6 | ]
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [leonmrbonnie]
4 | patreon: leonmrbonnie
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.code-workspace
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Open Source - Entity Sync for JS
2 |
3 | Created by LeonMrBonnie
4 |
5 | [❤️ Support me by becoming a Patron](https://www.patreon.com/leonmrbonnie/)
6 | ⭐ This repository if you found it useful!
7 |
8 | [](https://shields.io/)
9 |
10 | ---
11 |
12 | # Description
13 |
14 | This repository provides an alt:V resource to work with the C# Entity Sync in JS.
15 |
16 | It provides an easy base class for entities that you can expand for you own entities.
17 |
18 | This resource handles the showing/removing of entities for you, you only need to create you own entity types.
19 |
20 | ## Installing Dependencies / Installation
21 |
22 | **I cannot stress this enough. Ensure you have NodeJS 13+ or you will have problems.**
23 |
24 | - [NodeJS 13+](https://nodejs.org/en/download/current/)
25 | - An Existing or New Gamemode
26 | - General Scripting Knowledge
27 |
28 | First download the JS wrapper for the C# entity sync from [GitHub](https://github.com/Kudze/altv-csharp-entity-sync-to-js-wrapper).
29 | Then you need to build it using `dotnet publish -c Release` and create a new resource with the type `csharp`.
30 | You should name this resource `entitysync-wrapper`.
31 |
32 | After that simply add the name of this resource to your `server.cfg` resource section.
33 |
34 | `altv-os-js-entitysync`
35 |
36 | And also add the name of this resource to the `deps` array of your `resource.cfg` from your main gamemode.
37 | Also add the `entitysync-wrapper` to the `deps` array of your resource.
38 |
39 | Then simply clone this repository into your main server resources folder.
40 |
41 | ```
42 | cd resources
43 | git clone https://github.com/LeonMrBonnie/altv-os-js-entitysync
44 | ```
45 |
46 | Ensure your `package.json` includes this property:
47 |
48 | ```json
49 | "type": "module"
50 | ```
51 |
52 | # Adding new Entity types
53 |
54 | For every new entity type (textlabel, object, ped etc.) you need to create a new Class that extends the base Entity class.
55 | To do that you need to import this resource and extend from the `Entity` class. Then you can use the entity data etc.
56 | Make sure that every new Entity class has its own unique `type` id. Else it will cause conflicts. For more info take a look at the [Entity Sync Documentation](https://fabianterhorst.github.io/coreclr-module/articles/entity-sync.html)
57 |
58 | You can find an example in `server/textlabel-example.js` and `client/textlabel-example.js`
59 |
60 | ! IMPORTANT !
61 | All your files related to the entity sync have to be in the same resource, because you can't export and import a class.
62 | So you have to put all the files from this resource into your own resource.
63 |
64 | # Credits
65 |
66 | - [Stuyk](https://github.com/Stuyk) - For the drawtext3d function used in the example and this readme layout, as well as his other contributions to the alt:V community
67 | - [Fabian Terhorst aka. Heron](https://github.com/fabianterhorst) - For creating the C# module as well as creating the C# Entity Sync
68 | - [Kudze](https://github.com/Kudze) - For creating the JS wrapper for the C# Entity Sync
69 |
70 | # Other alt:V Open Source Resources
71 |
72 | - [Authentication by Stuyk](https://github.com/Stuyk/altv-os-auth)
73 | - [Discord Authentication by Stuyk](https://github.com/Stuyk/altv-discord-auth)
74 | - [Global Blip Manager by Dzeknjak](https://github.com/jovanivanovic/altv-os-global-blip-manager)
75 | - [Global Marker Manager by Dzeknjak](https://github.com/jovanivanovic/altv-os-global-marker-manager)
76 | - [Chat by Dzeknjak](https://github.com/jovanivanovic/altv-os-chat)
77 | - [Nametags by Stuyk](https://github.com/Stuyk/altv-os-nametags)
78 |
--------------------------------------------------------------------------------
/client/entitysync.js:
--------------------------------------------------------------------------------
1 | import * as alt from "alt-client";
2 |
3 | export class Entity {
4 | /**
5 | * An object mapped by id containing all entitiies
6 | *
7 | * @type {{}}
8 | * @static
9 | * @memberof Entity
10 | */
11 | static _entities = {};
12 |
13 | /**
14 | * An objected containing all entity type classes
15 | *
16 | * @static
17 | * @memberof Entity
18 | */
19 | static _types = {};
20 |
21 | /**
22 | * Gets entity by id
23 | *
24 | * @author LeonMrBonnie
25 | * @static
26 | * @param {Number} id
27 | * @param {Number} type
28 | * @returns {Entity}
29 | * @memberof Entity
30 | */
31 | static getEntity(id, type) {
32 | return Entity._entities[`${id}_${type}`];
33 | }
34 | /**
35 | * Adds an entity type class
36 | *
37 | * @author LeonMrBonnie
38 | * @static
39 | * @param {Number} id
40 | * @param {*} type Entity type class
41 | * @memberof Entity
42 | */
43 | static addType(id, type) {
44 | Entity._types[id] = type;
45 | }
46 | /**
47 | * Gets type by id
48 | *
49 | * @author LeonMrBonnie
50 | * @static
51 | * @param {Number} id
52 | * @returns
53 | * @memberof Entity
54 | */
55 | static getType(id) {
56 | return Entity._types[id];
57 | }
58 |
59 | /**
60 | * Creates an instance of Entity
61 | * @author LeonMrBonnie
62 | * @param {Number} id
63 | * @param {Number} type
64 | * @param {alt.Vector3} position
65 | * @param {{}} data
66 | * @memberof Entity
67 | */
68 | constructor(id, type, position, data) {
69 | this._id = id;
70 | this._type = type;
71 | this._position = position;
72 | this._data = data;
73 | this._visible = true;
74 | Entity._entities[`${id}_${type}`] = this;
75 | }
76 | destroy() {
77 | this.visible = false;
78 | delete Entity._entities[`${this.id}_${this.type}`];
79 | }
80 | get id() {
81 | return this._id;
82 | }
83 | get type() {
84 | return this._type;
85 | }
86 | get data() {
87 | return this._data;
88 | }
89 | set data(val) {
90 | this._data = val;
91 | }
92 | get visible() {
93 | return this._visible;
94 | }
95 | set visible(val) {
96 | this._visible = val;
97 | if(val) this?.show?.();
98 | else this?.hide?.();
99 | }
100 | get pos() {
101 | return this._position;
102 | }
103 | set pos(val) {
104 | this._position = val;
105 | this?.positionChanged?.();
106 | }
107 | }
108 |
109 | alt.onServer("entitySync:create", (id, type, pos, data) => {
110 | alt.log(`[ENTITY] Created entity ${id} (Type ${type})`);
111 | let entity = Entity.getEntity(id, type);
112 | if (entity) {
113 | // Entity already exists in cache, we only need to update the data
114 | if (data)
115 | for (let key in data) {
116 | entity.data[key] = data[key];
117 | }
118 | if (entity._position !== pos) entity.pos = pos;
119 | entity.visible = true;
120 | } else {
121 | // Entity doesn't exist in cache, create it
122 | let typeClass = Entity.getType(type);
123 | new typeClass(id, type, pos, data);
124 | }
125 | });
126 |
127 | alt.onServer("entitySync:remove", (id, type) => {
128 | alt.log(`[ENTITY] Removed entity ${id} (Type ${type})`);
129 | let entity = Entity.getEntity(id, type);
130 | if (!entity) return;
131 | entity.visible = false;
132 | });
133 |
134 | alt.onServer("entitySync:updatePosition", (id, type, pos) => {
135 | alt.log(`[ENTITY] Updated entity ${id} (Type ${type}) position`);
136 | let entity = Entity.getEntity(id, type);
137 | if (!entity) return;
138 | entity.pos = pos;
139 | });
140 |
141 | alt.onServer("entitySync:updateData", (id, type, data) => {
142 | alt.log(`[ENTITY] Updated entity ${id} (Type ${type}) data`);
143 | let entity = Entity.getEntity(id, type);
144 | if (!entity) return;
145 | for (let key in data) {
146 | entity.data[key] = data[key];
147 | }
148 | });
149 |
150 | alt.onServer("entitySync:clearCache", (id, type) => {
151 | let entity = Entity.getEntity(id, type);
152 | if (!entity) return;
153 | entity.destroy();
154 | });
155 |
156 | alt.onServer("entitySync:netOwner", (id, type, owner) => {
157 | alt.log(`[ENTITY] Now entity ${id} (Type ${type}) net owner`);
158 | let entity = Entity.getEntity(id, type);
159 | if (!entity) return;
160 | // TODO: Add net owner features
161 | });
162 |
163 | alt.on("disconnect", () => {
164 | for(let ent in Entity._entities) {
165 | let entity = Entity._entities[ent];
166 | if(entity) entity.destroy();
167 | }
168 | });
169 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import "./entitysync";
2 |
3 | // ***** EXAMPLE *****
4 | // Remove this if you don't want to use the example
5 | import "./textlabel-example";
--------------------------------------------------------------------------------
/client/textlabel-example.js:
--------------------------------------------------------------------------------
1 | import * as alt from "alt-client";
2 | import * as native from "natives";
3 | import { Entity } from "./entitysync";
4 |
5 | const TEXTLABEL_ENTITY_TYPE = 1;
6 |
7 | export default class Textlabel extends Entity {
8 | constructor(id, type, position, data) {
9 | // This is called when the label gets created
10 | // We should should show the entity here already, because this gets called the first time we enter the streaming range of the entity
11 | super(id, type, position, data);
12 | this.show();
13 | }
14 | destroy() {
15 | // This is called when the entity is completely removed from the world
16 | // Do some cleanup here like removing the every tick, deleting the object etc.
17 | super.destroy();
18 | if (this._tick) alt.clearEveryTick(this._tick);
19 | }
20 | get text() {
21 | return this.data.text;
22 | }
23 | get color() {
24 | return new alt.RGBA(this.data.r, this.data.g, this.data.b, this.data.a);
25 | }
26 | get scale() {
27 | return this.data.scale;
28 | }
29 | get font() {
30 | return this.data.font;
31 | }
32 | show() {
33 | // Called every time this entity is in streaming range, so we should create/show it
34 | this._tick = alt.everyTick(this.render.bind(this));
35 | }
36 | hide() {
37 | // Called every time this entity is now out of streaming range, so we should delete/hide it
38 | alt.clearEveryTick(this._tick);
39 | this._tick = null;
40 | }
41 | positionChanged() {
42 | // This is called every time the position of this entity has changed, we can update the position for our object etc. here with the new this.pos
43 | // Because we draw in every tick and not create in once, we do not need this function for textlabels
44 | }
45 | render() {
46 | // This is just an own custom function, which gets executed every tick and shows our textlabel in the world
47 | drawText3d(
48 | this.text,
49 | this.pos.x,
50 | this.pos.y,
51 | this.pos.z,
52 | this.scale,
53 | this.font,
54 | this.color.r,
55 | this.color.g,
56 | this.color.b,
57 | this.color.a
58 | );
59 | }
60 | }
61 |
62 | // Credits to Stuyk (https://github.com/Stuyk) for this function
63 | function drawText3d(
64 | msg,
65 | x,
66 | y,
67 | z,
68 | scale,
69 | fontType,
70 | r,
71 | g,
72 | b,
73 | a,
74 | useOutline = true,
75 | useDropShadow = true
76 | ) {
77 | let hex = msg.match("{.*}");
78 | if (hex) {
79 | const rgb = hexToRgb(hex[0].replace("{", "").replace("}", ""));
80 | r = rgb[0];
81 | g = rgb[1];
82 | b = rgb[2];
83 | msg = msg.replace(hex[0], "");
84 | }
85 |
86 | native.setDrawOrigin(x, y, z, 0);
87 | native.beginTextCommandDisplayText("CELL_EMAIL_BCON");
88 | msg.match(/.{1,99}/g).forEach((textBlock) => {
89 | native.addTextComponentSubstringPlayerName(textBlock);
90 | });
91 | native.setTextFont(fontType);
92 | native.setTextScale(1, scale);
93 | native.setTextWrap(0.0, 1.0);
94 | native.setTextCentre(true);
95 | native.setTextColour(r, g, b, a);
96 |
97 | if (useOutline) native.setTextOutline();
98 |
99 | if (useDropShadow) native.setTextDropShadow();
100 |
101 | native.endTextCommandDisplayText(0, 0, 0);
102 | native.clearDrawOrigin();
103 | }
104 | /**
105 | * Converts a hex color to an rgb color
106 | * @author Stuyk
107 | * @param {Number} hex Hex color
108 | * @returns {Array} Array containg R, G, B values
109 | */
110 | function hexToRgb(hex) {
111 | var bigint = parseInt(hex, 16);
112 | var r = (bigint >> 16) & 255;
113 | var g = (bigint >> 8) & 255;
114 | var b = bigint & 255;
115 | return [r, g, b];
116 | }
117 |
118 | Entity.addType(TEXTLABEL_ENTITY_TYPE, Textlabel);
119 |
--------------------------------------------------------------------------------
/postinstall.js:
--------------------------------------------------------------------------------
1 | /*
2 | THIS FILE IS FOR THE ALT:V INSTALLER POSTINSTALL!
3 | DO NOT INCLUDE IT IN YOUR PROJECT IF YOU INSTALL IT MANUALLY!
4 | */
5 |
6 | const COLORS = {
7 | Reset: "\x1b[0m",
8 | FgGreen: "\x1b[32m",
9 | FgCyan: "\x1b[36m",
10 | };
11 |
12 | console.log(`${COLORS.FgGreen}|-----------------------------------------|`);
13 | console.log(`${COLORS.FgGreen}|${COLORS.Reset} Thank you for installing my resource! ${COLORS.FgGreen}|`);
14 | console.log(`${COLORS.FgGreen}|${COLORS.Reset} Made by ${COLORS.FgCyan}LeonMrBonnie ${COLORS.FgGreen}|`);
15 | console.log(`${COLORS.FgGreen}|-----------------------------------------|`);
--------------------------------------------------------------------------------
/resource.cfg:
--------------------------------------------------------------------------------
1 | type: 'js',
2 |
3 | main: 'server/index.js',
4 | client-main: 'client/index.js',
5 | client-files: [ 'client/*' ],
6 | deps: [
7 | 'entitysync-wrapper'
8 | ]
--------------------------------------------------------------------------------
/server/entitysync.js:
--------------------------------------------------------------------------------
1 | import * as alt from "alt-server";
2 | import * as Entities from "entitysync-wrapper";
3 |
4 | export class Entity {
5 | /**
6 | * Creates an instance of Entity
7 | * @author LeonMrBonnie
8 | * @param {Number} type Entity type
9 | * @param {alt.Vector3} pos
10 | * @param {Number} dimension
11 | * @param {{}} data An object containing the entity data
12 | * @param {Number} range Entity stream range
13 | * @memberof Entity
14 | */
15 | constructor(type, pos, dimension, data, range) {
16 | this._type = type;
17 | this._entity = EntitySync.createEntity(
18 | type,
19 | pos,
20 | dimension,
21 | data,
22 | range
23 | );
24 | alt.log(`Created entity ${this._entity} with type ${type}`);
25 | }
26 | /**
27 | * Completely removes the entity
28 | *
29 | * @author LeonMrBonnie
30 | * @memberof Entity
31 | */
32 | destroy() {
33 | EntitySync.removeEntity(this._entity, this._type);
34 | alt.log(`Removed entity ${this._entity} with type ${this._type}`);
35 | }
36 | /**
37 | * Gets entity data by key
38 | *
39 | * @author LeonMrBonnie
40 | * @param {String} key
41 | * @returns {*}
42 | * @memberof Entity
43 | */
44 | getData(key) {
45 | return EntitySync.getEntityData(this._entity, this._type, key);
46 | }
47 | /**
48 | * Sets entity data by key
49 | *
50 | * @author LeonMrBonnie
51 | * @param {String} key
52 | * @param {*} value
53 | * @memberof Entity
54 | */
55 | setData(key, value) {
56 | EntitySync.setEntityData(this._entity, this._type, key, value);
57 | }
58 | get pos() {
59 | return EntitySync.getEntityPosition(this._entity, this._type);
60 | }
61 | set pos(val) {
62 | EntitySync.setEntityPosition(this._entity, this._type, val);
63 | }
64 | get dimension() {
65 | return EntitySync.getEntityDimension(this._entity, this._type);
66 | }
67 | set dimension(val) {
68 | EntitySync.setEntityDimension(this._entity, this._type, val);
69 | }
70 | }
71 |
72 | export class EntitySync {
73 | /**
74 | * Creates a new entity
75 | *
76 | * @author LeonMrBonnie
77 | * @static
78 | * @param {Number} type
79 | * @param {alt.Vector3} pos
80 | * @param {Number} dimension
81 | * @param {{}} data
82 | * @param {Number} range
83 | * @returns {Number} Entity id
84 | * @memberof EntitySync
85 | */
86 | static createEntity(type, pos, dimension, data, range) {
87 | let entity = Entities.createGameEntity(type, pos, dimension, range);
88 | for (let key in data)
89 | Entities.setGameEntityData(entity, type, key, data[key]);
90 | return entity;
91 | }
92 |
93 | /**
94 | * Removes the specified entity
95 | *
96 | * @author LeonMrBonnie
97 | * @static
98 | * @param {Number} id
99 | * @param {Number} type
100 | * @memberof EntitySync
101 | */
102 | static removeEntity(id, type) {
103 | Entities.removeGameEntity(id, type);
104 | }
105 |
106 | /**
107 | * Sets entity data by key
108 | *
109 | * @author LeonMrBonnie
110 | * @static
111 | * @param {Number} id
112 | * @param {Number} type
113 | * @param {String} key
114 | * @param {*} value
115 | * @memberof EntitySync
116 | */
117 | static setEntityData(id, type, key, value) {
118 | Entities.setGameEntityData(id, type, key, value);
119 | }
120 |
121 | /**
122 | * Gets entity data by key
123 | *
124 | * @author LeonMrBonnie
125 | * @static
126 | * @param {Number} id
127 | * @param {Number} type
128 | * @param {String} key
129 | * @returns {*}
130 | * @memberof EntitySync
131 | */
132 | static getEntityData(id, type, key) {
133 | return Entities.getGameEntityData(id, type, key);
134 | }
135 |
136 | /**
137 | * Sets the entity position
138 | *
139 | * @author LeonMrBonnie
140 | * @static
141 | * @param {Number} id
142 | * @param {Number} type
143 | * @param {alt.Vector3} pos
144 | * @memberof EntitySync
145 | */
146 | static setEntityPosition(id, type, pos) {
147 | Entities.setGameEntityPosition(id, type, pos);
148 | }
149 |
150 | /**
151 | * Gets the entity position
152 | *
153 | * @author LeonMrBonnie
154 | * @static
155 | * @param {Number} id
156 | * @param {Number} type
157 | * @returns {alt.Vector3}
158 | * @memberof EntitySync
159 | */
160 | static getEntityPosition(id, type) {
161 | return Entities.getGameEntityPosition(id, type);
162 | }
163 |
164 | /**
165 | * Gets the entity stream range
166 | *
167 | * @author LeonMrBonnie
168 | * @static
169 | * @param {Number} id
170 | * @param {Number} type
171 | * @returns {Number}
172 | * @memberof EntitySync
173 | */
174 | static getEntityRange(id, type) {
175 | return Entities.getGameEntityRange(id, type);
176 | }
177 |
178 | /**
179 | * Sets the entity dimension
180 | *
181 | * @author LeonMrBonnie
182 | * @static
183 | * @param {Number} id
184 | * @param {Number} type
185 | * @param {Number} dimension
186 | * @memberof EntitySync
187 | */
188 | static setEntityDimension(id, type, dimension) {
189 | Entities.setGameEntityDimension(id, type, dimension);
190 | }
191 |
192 | /**
193 | * Gets the entity dimension
194 | *
195 | * @author LeonMrBonnie
196 | * @static
197 | * @param {Number} id
198 | * @param {Number} type
199 | * @returns
200 | * @memberof EntitySync
201 | */
202 | static getEntityDimension(id, type) {
203 | return Entities.getGameEntityDimension(id, type);
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import * as EntitySync from "./entitysync";
2 |
3 | export { EntitySync };
4 |
5 | // ***** EXAMPLE *****
6 | // Remove this if you don't want to use the example
7 | import * as alt from "alt-server";
8 | import Textlabel from "./textlabel-example";
9 |
10 | alt.on("playerConnect", (player) => {
11 | player.model = "mp_m_freemode_01";
12 | player.spawn(0, 0, 70); // Spawns the player
13 | player.label = new Textlabel(`~g~${player.name}`, 4, 0.5, new alt.Vector3(0, 0, 70)); // Creates a new textlabel with the player name as the text
14 | });
--------------------------------------------------------------------------------
/server/textlabel-example.js:
--------------------------------------------------------------------------------
1 | import * as alt from "alt-server";
2 | import * as EntitySync from "./entitysync";
3 |
4 | const TEXTLABEL_ENTITY_TYPE = 1;
5 |
6 | export default class Textlabel extends EntitySync.Entity {
7 | /**
8 | * Creates an instance of Textlabel
9 | * @author LeonMrBonnie
10 | * @param {String} text
11 | * @param {Number} font
12 | * @param {Number} scale
13 | * @param {alt.Vector3} pos
14 | * @param {Number} dimension
15 | * @param {alt.RGBA} color
16 | * @param {number} [range=5]
17 | * @memberof Textlabel
18 | */
19 | constructor(
20 | text,
21 | font,
22 | scale,
23 | pos,
24 | dimension = 0,
25 | color = new alt.RGBA(0, 0, 0, 255),
26 | range = 10
27 | ) {
28 | super(
29 | TEXTLABEL_ENTITY_TYPE,
30 | pos,
31 | dimension,
32 | {
33 | text: text,
34 | r: color.r,
35 | g: color.g,
36 | b: color.b,
37 | a: color.a,
38 | scale: scale,
39 | font: font,
40 | },
41 | range
42 | );
43 | }
44 | get text() {
45 | return this.getData("text");
46 | }
47 | set text(val) {
48 | this.setData("text", val);
49 | }
50 | get color() {
51 | return new alt.RGBA(
52 | this.getData("r"),
53 | this.getData("g"),
54 | this.getData("b"),
55 | this.getData("a")
56 | );
57 | }
58 | set color(val) {
59 | this.setData("r", val.r);
60 | this.setData("g", val.g);
61 | this.setData("b", val.b);
62 | this.setData("a", val.a);
63 | }
64 | get scale() {
65 | return this.getData("scale");
66 | }
67 | set scale(val) {
68 | this.setData("scale", val);
69 | }
70 | get font() {
71 | return this.getData("font");
72 | }
73 | set font(val) {
74 | this.setData("font", val);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------