Created
October 18, 2024 18:57
-
-
Save root-cause/2a829cf29dffa849452d36cc3cdc91c3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const NUM_COMPONENT_TYPES = 16; | |
const ENTITY_TYPE_PLAYER = 0; | |
const GET_SHOP_PED_COMPONENT = 0x74C0E2A57EC66760n; | |
const GET_SHOP_PED_PROP = 0x5D5CAFF661DDF6FCn; | |
function isBitSet(num, position) { | |
return (num & (1 << position)) !== 0; | |
} | |
function applyComponentsFromBuffer(player, componentBytes) { | |
if (!mp.players.exists(player) || player.handle === 0 || componentBytes == null) { | |
// invalid player or no component data | |
return; | |
} | |
const reader = new DataView(componentBytes); | |
let readOffset = 0; | |
// read component bits | |
const componentBits = reader.getInt16(readOffset); | |
readOffset += Int16Array.BYTES_PER_ELEMENT; | |
if (componentBits === 0) { | |
// nothing is set, no need to go further | |
return; | |
} | |
// read and apply set components | |
// this can be smarter and apply only the changed components... I guess | |
let buffer = [ new ArrayBuffer(136) ]; | |
for (let componentType = 0; componentType < NUM_COMPONENT_TYPES; componentType++) { | |
if (!isBitSet(componentBits, componentType)) { | |
continue; | |
} | |
const isProp = componentType >= 11; | |
const hash = reader.getInt32(readOffset); | |
readOffset += Int32Array.BYTES_PER_ELEMENT; | |
if (!mp.game.invoke((isProp ? GET_SHOP_PED_PROP : GET_SHOP_PED_COMPONENT), hash, buffer)) { | |
continue; | |
} | |
const componentView = new DataView(buffer[0]); | |
const drawable = componentView.getInt32(24, true); | |
const texture = componentView.getInt32(32, true); | |
const slot = componentView.getInt32(48, true); | |
if (isProp) { | |
player.setPropIndex(slot, drawable, texture, false); | |
} else { | |
player.setComponentVariation(slot, drawable, texture, 0); | |
} | |
} | |
} | |
// Event handlers | |
mp.events.add("entityStreamIn", (entity) => { | |
if (entity.typeInt === ENTITY_TYPE_PLAYER) { | |
applyComponentsFromBuffer(entity, entity.getVariable("_componentBytes")); | |
} | |
}); | |
mp.events.addDataHandler("_componentBytes", (entity, newComponentBytes) => { | |
if (entity.typeInt === ENTITY_TYPE_PLAYER && entity.handle !== 0) { | |
applyComponentsFromBuffer(entity, newComponentBytes); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// https://wiki.rage.mp/index.php?title=Clothes except clothing and prop types are combined | |
const ComponentType = Object.freeze({ | |
// clothes | |
HEAD_UNUSED: 0, // head | |
BERD: 1, // mask | |
HAIR: 2, // hair style | |
UPPR: 3, // torso | |
LOWR: 4, // pants | |
HAND: 5, // bags and parachutes | |
FEET: 6, // shoes | |
TEEF: 7, // accessories | |
ACCS: 8, // undershirts | |
TASK: 9, // body armors | |
DECL: 10, // decals | |
JBIB: 11, // tops | |
// props | |
HEAD: 12, // hats | |
EYES: 13, // glasses | |
EARS: 14, // earrings | |
LEFT_WRIST: 15, // watches | |
RIGHT_WRIST: 16, // bracelets | |
}); | |
// Give yourself the N.O.O.S.E. outfit (as mp_m_freemode_01) | |
mp.events.addCommand("swat", player => { | |
// put on the mask | |
player.setComponentHash(ComponentType.BERD, mp.joaat("DLC_MP_HEIST_M_BERD_17_0")); | |
// ...then the rest | |
player.setComponentHashes([ | |
[ComponentType.UPPR, mp.joaat("DLC_MP_LTS_M_UPPR_0_0")], | |
[ComponentType.LOWR, mp.joaat("DLC_MP_H3_M_LEGS_1_0")], | |
[ComponentType.FEET, mp.joaat("DLC_MP_PILOT_M_FEET_0_0")], | |
[ComponentType.DECL, mp.joaat("DLC_MP_H3_M_DECL_6_1")], | |
[ComponentType.JBIB, mp.joaat("DLC_MP_H3_M_JBIB_6_0")], | |
[ComponentType.HEAD, mp.joaat("DLC_MP_H3_M_PHEAD_4_0")] | |
]); | |
// since this undershirt doesn't have a hash, we're using its id | |
player.setClothes(8, 15, 0, 0); | |
}); | |
// Remove your mask | |
mp.events.addCommand("face_reveal", player => { | |
player.setComponentHash(ComponentType.BERD, 0); | |
// removals still need setClothes/setProp to be called | |
player.setClothes(ComponentType.BERD, 0, 0, 0); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const NUM_COMPONENT_TYPES = 16; | |
function setBit(num, position) { | |
return num | (1 << position); | |
} | |
function setComponentHashInternal(player, componentType, hash) { | |
if (!Number.isInteger(componentType)) { | |
throw new TypeError("'componentType' must be an integer"); | |
} else if (!Number.isInteger(hash)) { | |
throw new TypeError("'hash' must be an integer"); | |
} else if (componentType < 1 || componentType > NUM_COMPONENT_TYPES) { | |
throw new RangeError(`'componentType' must be between 1 and ${NUM_COMPONENT_TYPES}`); | |
} | |
// component 0 is the head which is set by player.setCustomization, we'll use it to store masks instead | |
player.__components[componentType - 1] = hash >> 0; | |
} | |
function serializeComponents(components) { | |
let componentBits = 0; | |
// calculate required buffer size as well as setting active component bits | |
let bufferSize = Int16Array.BYTES_PER_ELEMENT; | |
for (let i = 0; i < components.length; i++) { | |
if (components[i] !== 0) { | |
componentBits = setBit(componentBits, i); | |
bufferSize += Int32Array.BYTES_PER_ELEMENT; | |
} | |
} | |
const buffer = new ArrayBuffer(bufferSize); | |
const writer = new DataView(buffer); | |
let writerOffset = 0; | |
// write set bits | |
writer.setInt16(writerOffset, componentBits); | |
writerOffset += Int16Array.BYTES_PER_ELEMENT; | |
// write hashes | |
for (const componentHash of components) { | |
if (componentHash !== 0) { | |
writer.setInt32(writerOffset, componentHash); | |
writerOffset += Int32Array.BYTES_PER_ELEMENT; | |
} | |
} | |
return buffer; | |
} | |
// Player functions | |
mp.Player.prototype.setComponentHash = function(componentType, hash) { | |
setComponentHashInternal(this, componentType, hash); | |
this.setVariable("_componentBytes", serializeComponents(this.__components)); | |
} | |
mp.Player.prototype.getComponentHash = function(componentType) { | |
if (!Number.isInteger(componentType)) { | |
throw new TypeError("'componentType' must be an integer"); | |
} else if (componentType < 1 || componentType > NUM_COMPONENT_TYPES) { | |
throw new RangeError(`'componentType' must be between 1 and ${NUM_COMPONENT_TYPES}`); | |
} | |
return this.__components[componentType - 1]; | |
} | |
mp.Player.prototype.setComponentHashes = function(components) { | |
if (!Array.isArray(components)) { | |
throw new TypeError("'components' must be an array"); | |
} | |
for (const item of components) { | |
const [componentType, hash] = item; | |
setComponentHashInternal(this, componentType, hash); | |
} | |
this.setVariable("_componentBytes", serializeComponents(this.__components)); | |
} | |
// RAGEMP events | |
function onPlayerJoin(player) { | |
player.__components = new Int32Array(NUM_COMPONENT_TYPES); | |
} | |
function onPlayerQuit(player) { | |
delete player.__components; | |
} | |
mp.events.add({ | |
"playerJoin": onPlayerJoin, | |
"playerQuit": onPlayerQuit | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment