MedicalListeners lets you attach custom logic to the medical lifecycle without editing the core encrypted files.
Files:
modules/medical_listeners/client.luamodules/medical_listeners/server.lua
Keep listeners non-blocking. Do not use Wait or long loops inside these functions because they can run during death-related flows.
Available Functions#
The listener names below are defined on both client and server:
MedicalListeners.onPlayerRevive(data)MedicalListeners.onPlayerRecovered(data)MedicalListeners.onPlayerHeal(data)MedicalListeners.onPlayerDeath(data)MedicalListeners.onPlayerLaststand(data)MedicalListeners.onPlayerRespawn(data)MedicalListeners.onPlayerSpawn(data)MedicalListeners.onPlayerDeathStateChange(data)MedicalListeners.onBillingInvoiceCreated(data)MedicalListeners.onBillingInvoicePaid(data)MedicalListeners.onBillingInvoiceCancelled(data)MedicalListeners.onBillingExternalPaymentReceived(data)
Emission Notes#
Currently emitted on both client and server:
onPlayerReviveonPlayerRecoveredonPlayerDeathonPlayerLaststandonPlayerDeathStateChange
Currently emitted client-side only:
onPlayerHealonPlayerRespawnonPlayerSpawn
Server-side functions for the client-only entries can still exist safely, but the current core does not emit those three listener names on the server.
Currently emitted server-side only:
onBillingInvoiceCreatedonBillingInvoicePaidonBillingInvoiceCancelledonBillingExternalPaymentReceived
Listener Semantics#
onPlayerRevive#
Triggered when a downed player is revived back to alive. Use this for revive-specific integrations.
onPlayerRecovered#
Triggered when a player returns from dead or laststand to alive. This is broader than revive and can include respawn or sync recovery flows.
onPlayerHeal#
Triggered on the heal branch. Use this for full-heal integrations that are not tied to death recovery.
onPlayerDeath#
Triggered when the player enters dead.
onPlayerLaststand#
Triggered when the player enters laststand.
onPlayerRespawn#
Triggered when the player respawns through the hospital respawn flow.
onPlayerSpawn#
Triggered on framework / game spawn sync points.
onPlayerDeathStateChange#
Triggered on every real death-state transition, such as alive->laststand, laststand->dead, or dead->alive.
onBillingInvoiceCreated#
Triggered after a facility invoice is created and any external invoice id is saved.
onBillingInvoicePaid#
Triggered after a facility invoice is marked paid, facility account deposit is processed, and the facility log is written.
onBillingInvoiceCancelled#
Triggered after an unpaid facility invoice is cancelled.
onBillingExternalPaymentReceived#
Triggered when the external billing bridge reports payment for a matching invoice. This is emitted before the invoice is marked paid by the bridge callback.
Medical Listener Payload#
Each medical lifecycle listener receives a single data table.
Type:
MedicalListenerPayload
Type file:
modules/types/medical_listeners.lua
Fields:
sourcetargetpreviousStatenewStatereasoncontext
Field Descriptions#
source: The actor or source that triggered the action. In command-based revives this can be the admin or EMS player. In local or sync-driven flows this can be the player themselves.
target: The player affected by the medical action.
previousState: The previous medical state. Possible values are alive, laststand, and dead.
newState: The new medical state. Possible values are alive, laststand, and dead.
reason: A short string describing why the listener was triggered.
context: Optional table with extra information about the trigger. The content depends on the flow.
Billing Listener Payload#
Billing listeners receive a billing-specific data table.
Fields:
invoicesourcefacility_idinvoice_idinvoice_nopatient_identifierpatient_namedoctor_identifierdoctor_nameamountreasonstatusexternal_providerexternal_invoice_idcontext
invoice contains the mapped invoice row, including:
idfacility_idinvoice_nopatient_identifierpatient_namedoctor_identifierdoctor_nameamountreasonpreset_keystatusexternal_providerexternal_invoice_idpaid_atcreated_atupdated_at
Billing listener context.action can be created, paid, cancelled, or external_paid.
Reason Constants#
Use the shared constants instead of hardcoding strings when possible:
MedicalListeners.REASONS.ADMIN_COMMAND
MedicalListeners.REASONS.EMS_REVIVE
MedicalListeners.REASONS.NPC_DOCTOR
MedicalListeners.REASONS.HOSPITAL_RESPAWN
MedicalListeners.REASONS.FRAMEWORK_SYNC
MedicalListeners.REASONS.FRAMEWORK_SPAWN_SYNC
MedicalListeners.REASONS.STATEBAG_SYNC
MedicalListeners.REASONS.LOAD_SYNC
MedicalListeners.REASONS.DAMAGE
MedicalListeners.REASONS.ADMIN_DAMAGE
MedicalListeners.REASONS.LASTSTAND_TIMEOUT
MedicalListeners.REASONS.LASTSTAND_CRAWL_LIMIT
MedicalListeners.REASONS.DISCONNECT
MedicalListeners.REASONS.CLIENT_SYNC
MedicalListeners.REASONS.COMPAT_EXPORT
Notes:
- The constants table covers the canonical shared reasons.
- Some runtime features may pass additional raw reasons such as
health_system.
Helper Functions#
These helpers are available in MedicalListeners:
MedicalListeners.isAliveState(state)MedicalListeners.isDownedState(state)MedicalListeners.isRecovery(data)MedicalListeners.isRevive(data)MedicalListeners.isRespawn(data)MedicalListeners.matchesReason(data, reason)MedicalListeners.getTransitionKey(data)
Helper Examples#
if MedicalListeners.isRecovery(data) then
print(("Recovered via %s"):format(data.reason))
end
if MedicalListeners.matchesReason(data, MedicalListeners.REASONS.NPC_DOCTOR) then
print("Recovery came from NPC doctor")
end
Client Examples#
Play a custom effect on laststand#
function MedicalListeners.onPlayerLaststand(data)
if data.reason == MedicalListeners.REASONS.DAMAGE then
ShakeGameplayCam("SMALL_EXPLOSION_SHAKE", 0.15)
end
end
React only to hospital respawn#
function MedicalListeners.onPlayerRespawn(data)
if MedicalListeners.isRespawn(data) then
print(("Respawned at facility %s"):format(data.context and data.context.facilityId or "unknown"))
end
end
Server Examples#
Log revive source and target#
function MedicalListeners.onPlayerRevive(data)
print(("Revive source=%s target=%s reason=%s"):format(
tostring(data.source),
tostring(data.target),
tostring(data.reason)
))
end
Trigger another resource only for EMS revives#
function MedicalListeners.onPlayerRevive(data)
if not MedicalListeners.matchesReason(data, MedicalListeners.REASONS.EMS_REVIVE) then
return
end
TriggerEvent("my-resource:server:onEmsRevive", data.target, data.source)
end
Handle all recovery paths from one place#
function MedicalListeners.onPlayerRecovered(data)
if MedicalListeners.isRespawn(data) then
TriggerEvent("my-resource:server:onHospitalRecovery", data.target)
return
end
TriggerEvent("my-resource:server:onGeneralRecovery", data.target, data.reason)
end
Listen for paid facility invoices#
function MedicalListeners.onBillingInvoicePaid(data)
TriggerEvent("my-resource:server:onMedicalInvoicePaid", {
invoiceId = data.invoice_id,
facilityId = data.facility_id,
patient = data.patient_identifier,
amount = data.amount,
})
end
Example Payloads#
Revive#
{
source = 1,
target = 24,
previousState = "dead",
newState = "alive",
reason = "admin_command",
context = {
from = "commands:adminRevive"
}
}
Death#
{
source = 24,
target = 24,
previousState = "laststand",
newState = "dead",
reason = "laststand_timeout",
context = {
from = "laststandTimer"
}
}
Respawn#
{
source = 24,
target = 24,
previousState = "dead",
newState = "alive",
reason = "hospital_respawn",
context = {
facilityId = 3,
respawnPoint = {
x = 0.0,
y = 0.0,
z = 0.0,
w = 0.0
}
}
}
Billing Invoice Paid#
{
invoice_id = 12,
invoice_no = "EMS-1-260502101530-0042",
facility_id = 1,
patient_identifier = "license:xxxx",
patient_name = "John Doe",
amount = 750,
reason = "Revive",
status = "paid",
external_provider = "manual",
external_invoice_id = "",
context = {
action = "paid",
invoice_id = 12
}
}
Notes#
contextis not a fixed schema. It changes depending on the action.sourcedoes not always mean "the player who revived someone". In sync-based flows it may represent the affected player or system-originated action.- Billing listeners are defined in both client and server files for consistency, but they are emitted server-side only.