├── .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 | [![Generic badge](https://img.shields.io/badge/.altv_Installer%3F-Yes!-4E753E.svg)](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 | --------------------------------------------------------------------------------