# Medical Listeners

`MedicalListeners` lets you attach custom logic to the medical lifecycle without editing the core encrypted files.

Files:

* `modules/medical_listeners/client.lua`
* `modules/medical_listeners/server.lua`

{% hint style="warning" %}
Keep listeners non-blocking. Do not use `Wait` or long loops inside these functions because they can run during death-related flows.
{% endhint %}

## 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:

* `onPlayerRevive`
* `onPlayerRecovered`
* `onPlayerDeath`
* `onPlayerLaststand`
* `onPlayerDeathStateChange`

Currently emitted client-side only:

* `onPlayerHeal`
* `onPlayerRespawn`
* `onPlayerSpawn`

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:

* `onBillingInvoiceCreated`
* `onBillingInvoicePaid`
* `onBillingInvoiceCancelled`
* `onBillingExternalPaymentReceived`

## 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:

* `source`
* `target`
* `previousState`
* `newState`
* `reason`
* `context`

### 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:

* `invoice`
* `source`
* `facility_id`
* `invoice_id`
* `invoice_no`
* `patient_identifier`
* `patient_name`
* `doctor_identifier`
* `doctor_name`
* `amount`
* `reason`
* `status`
* `external_provider`
* `external_invoice_id`
* `context`

`invoice` contains the mapped invoice row, including:

* `id`
* `facility_id`
* `invoice_no`
* `patient_identifier`
* `patient_name`
* `doctor_identifier`
* `doctor_name`
* `amount`
* `reason`
* `preset_key`
* `status`
* `external_provider`
* `external_invoice_id`
* `paid_at`
* `created_at`
* `updated_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:

```lua
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

```lua
if MedicalListeners.isRecovery(data) then
    print(("Recovered via %s"):format(data.reason))
end
```

```lua
if MedicalListeners.matchesReason(data, MedicalListeners.REASONS.NPC_DOCTOR) then
    print("Recovery came from NPC doctor")
end
```

## Client Examples

### Play a custom effect on laststand

```lua
function MedicalListeners.onPlayerLaststand(data)
    if data.reason == MedicalListeners.REASONS.DAMAGE then
        ShakeGameplayCam("SMALL_EXPLOSION_SHAKE", 0.15)
    end
end
```

### React only to hospital respawn

```lua
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

```lua
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

```lua
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

```lua
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

```lua
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

```lua
{
    source = 1,
    target = 24,
    previousState = "dead",
    newState = "alive",
    reason = "admin_command",
    context = {
        from = "commands:adminRevive"
    }
}
```

### Death

```lua
{
    source = 24,
    target = 24,
    previousState = "laststand",
    newState = "dead",
    reason = "laststand_timeout",
    context = {
        from = "laststandTimer"
    }
}
```

### Respawn

```lua
{
    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

```lua
{
    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

* `context` is not a fixed schema. It changes depending on the action.
* `source` does 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.0resmon.org/0resmon/0r-resources/0r-ambulancejob/medical-listeners.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
